Kennenlernen der MutationObserver-API

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ Die Überwachung auf Änderungen am DOM ist manchmal in komplexen Web-Apps und Frameworks erforderlich. Anhand von Erklärungen und interaktiven Demos zeigt Ihnen dieser Artikel, wie Sie die MutationObserver-API verwenden können, um das Beobachten von DOM-Änderungen relativ einfach zu gestalten.

In komplexen Web-Apps können DOM-Änderungen häufig sein. Daher gibt es Fälle, in denen Ihre App möglicherweise auf eine bestimmte Änderung am DOM reagieren muss.

Für einige Zeit war die akzeptierte Methode, nach Änderungen am DOM zu suchen, eine Funktion namens Mutation Events, die jetzt veraltet ist. Der vom W3C genehmigte Ersatz für Mutationsereignisse ist die MutationObserver-API, auf die ich in diesem Artikel ausführlich eingehen werde.

Eine Reihe älterer Artikel und Referenzen diskutieren, warum das alte Feature ersetzt wurde, daher werde ich hier nicht näher darauf eingehen (abgesehen von der Tatsache, dass ich dem nicht gerecht werden könnte). Die MutationObserver API bietet nahezu vollständige Browserunterstützung, sodass wir sie bei Bedarf in den meisten – wenn nicht allen – Projekten sicher verwenden können.

Grundlegende Syntax für einen MutationObserver

Ein MutationObserver kann auf verschiedene Arten verwendet werden, die ich im Rest dieses Artikels ausführlich behandeln werde, aber die grundlegende Syntax für einen MutationObserver sieht folgendermaßen aus:

 let observer = new MutationObserver(callback); function callback (mutations) { // do something here } observer.observe(targetNode, observerOptions);

Die erste Zeile erstellt einen neuen MutationObserver mit dem Konstruktor MutationObserver() . Das an den Konstruktor übergebene Argument ist eine Callback-Funktion, die bei jeder qualifizierten DOM-Änderung aufgerufen wird.

Der Weg, um zu bestimmen, was für einen bestimmten Beobachter geeignet ist, erfolgt anhand der letzten Zeile im obigen Code. In dieser Zeile verwende ich die Methode observe() des MutationObserver , um mit der Beobachtung zu beginnen. Sie können dies mit etwas wie addEventListener() vergleichen. Sobald Sie einen Listener anhängen, „lauscht“ die Seite auf das angegebene Ereignis. Wenn Sie mit der Beobachtung beginnen, beginnt die Seite in ähnlicher Weise mit der Beobachtung für den angegebenen MutationObserver .

Mehr nach dem Sprung! Lesen Sie unten weiter ↓

Die Methode observe() nimmt zwei Argumente entgegen: Das Ziel , das der Knoten oder Knotenbaum sein sollte, auf dem Änderungen beobachtet werden sollen; und ein options -Objekt, bei dem es sich um ein MutationObserverInit -Objekt handelt, mit dem Sie die Konfiguration für den Beobachter definieren können.

Das letzte wichtige Grundmerkmal eines MutationObserver ist die Methode disconnect() . Dadurch können Sie die Beobachtung der angegebenen Änderungen beenden, und es sieht so aus:

 observer.disconnect();

Optionen zum Konfigurieren eines MutationObserver

Wie bereits erwähnt, erfordert die Methode observe() eines MutationObserver ein zweites Argument, das die Optionen zur Beschreibung des MutationObserver angibt. So würde das Optionsobjekt mit allen möglichen Eigenschafts-/Wertpaaren aussehen:

 let options = { childList: true, attributes: true, characterData: false, subtree: false, attributeFilter: ['one', 'two'], attributeOldValue: false, characterDataOldValue: false };

Beim Einrichten der MutationObserver -Optionen ist es nicht erforderlich, alle diese Zeilen einzubeziehen. Ich füge diese nur zu Referenzzwecken hinzu, damit Sie sehen können, welche Optionen verfügbar sind und welche Arten von Werten sie annehmen können. Wie Sie sehen können, sind alle bis auf einen booleschen Wert.

Damit ein MutationObserver funktioniert, muss mindestens eines von childList , attributes oder characterData auf true gesetzt werden, andernfalls wird ein Fehler ausgegeben. Die anderen vier Eigenschaften arbeiten in Verbindung mit einer dieser drei (dazu später mehr).

Bisher habe ich nur die Syntax beschönigt, um Ihnen einen Überblick zu geben. Der beste Weg, um zu sehen, wie jede dieser Funktionen funktioniert, besteht darin, Codebeispiele und Live-Demos bereitzustellen, die die verschiedenen Optionen beinhalten. Das werde ich für den Rest dieses Artikels tun.

Beobachten von Änderungen an untergeordneten Elementen mithilfe von childList

Der erste und einfachste MutationObserver , den Sie initiieren können, ist einer, der nach untergeordneten Knoten eines bestimmten Knotens (normalerweise eines Elements) sucht, die hinzugefügt oder entfernt werden sollen. In meinem Beispiel werde ich eine ungeordnete Liste in meinem HTML erstellen und ich möchte wissen, wann ein untergeordneter Knoten zu diesem Listenelement hinzugefügt oder daraus entfernt wird.

Der HTML-Code für die Liste sieht folgendermaßen aus:

 <ul class="list"> <li>Apples</li> <li>Oranges</li> <li>Bananas</li> <li class="child">Peaches</li> </ul>

Das JavaScript für my MutationObserver enthält Folgendes:

 let mList = document.getElementById('myList'), options = { childList: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'childList') { console.log('Mutation Detected: A child node has been added or removed.'); } } } observer.observe(mList, options);

Dies ist nur ein Teil des Codes. Der Kürze halber zeige ich die wichtigsten Abschnitte, die sich mit der MutationObserver API selbst befassen.

Beachten Sie, wie ich das mutations Argument durchlaufe, bei dem es sich um ein MutationRecord Objekt mit einer Reihe unterschiedlicher Eigenschaften handelt. In diesem Fall lese ich die type Eigenschaft und protokolliere eine Meldung, die angibt, dass der Browser eine geeignete Mutation entdeckt hat. Beachten Sie auch, wie ich das mList Element (eine Referenz auf meine HTML-Liste) als Zielelement übergebe (dh das Element, das ich auf Änderungen beobachten möchte).

  • Sehen Sie sich die vollständige interaktive Demo an →

Verwenden Sie die Schaltflächen, um den MutationObserver zu starten und zu stoppen. Die Protokollmeldungen helfen zu klären, was passiert. Kommentare im Code bieten auch einige Erklärungen.

Beachten Sie hier einige wichtige Punkte:

  • Die Callback-Funktion (die ich mCallback genannt habe, um zu veranschaulichen, dass Sie sie beliebig benennen können) wird jedes Mal ausgelöst, wenn eine erfolgreiche Mutation erkannt wird und nachdem die Methode Observe observe() ausgeführt wurde.
  • In meinem Beispiel ist childList der einzige Mutationstyp, der sich qualifiziert, daher ist es sinnvoll, beim Durchlaufen des MutationRecord nach diesem zu suchen. Die Suche nach einem anderen Typ würde in diesem Fall nichts bewirken (die anderen Typen werden in nachfolgenden Demos verwendet).
  • Mit childList kann ich einen Textknoten zum Zielelement hinzufügen oder daraus entfernen, und auch dies würde sich qualifizieren. Es muss also kein Element sein, das hinzugefügt oder entfernt wird.
  • In diesem Beispiel kommen nur unmittelbar untergeordnete Knoten in Frage. Später in diesem Artikel zeige ich Ihnen, wie dies für alle untergeordneten Knoten, Enkel usw. gelten kann.

Beobachten von Änderungen an den Attributen eines Elements

Eine andere gängige Art von Mutation, die Sie möglicherweise nachverfolgen möchten, ist die Änderung eines Attributs für ein bestimmtes Element. In der nächsten interaktiven Demo werde ich Änderungen an Attributen eines Absatzelements beobachten.

 let mPar = document.getElementById('myParagraph'), options = { attributes: true }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } } observer.observe(mPar, options);
  • Probieren Sie die Demo aus →

Auch hier habe ich den Code der Übersichtlichkeit halber abgekürzt, aber die wichtigen Teile sind:

  • Das options -Objekt verwendet die attributes -Eigenschaft, die auf true gesetzt ist, um dem MutationObserver mitzuteilen, dass ich nach Änderungen an den Attributen des Zielelements suchen möchte.
  • Der Mutationstyp, auf den ich in meiner Schleife teste, ist attributes , der einzige, der sich in diesem Fall qualifiziert.
  • Ich verwende auch die Eigenschaft attributeName des mutation , mit der ich herausfinden kann, welches Attribut geändert wurde.
  • Wenn ich den Beobachter auslöse, übergebe ich das Absatzelement als Referenz zusammen mit den Optionen.

In diesem Beispiel wird eine Schaltfläche verwendet, um einen Klassennamen auf dem Ziel-HTML-Element umzuschalten. Die Callback-Funktion im Mutation Observer wird jedes Mal ausgelöst, wenn die Klasse hinzugefügt oder entfernt wird.

Beobachten von Zeichendatenänderungen

Eine weitere Änderung, nach der Sie möglicherweise in Ihrer App suchen möchten, sind Mutationen an Zeichendaten. das heißt, Änderungen an einem bestimmten Textknoten. Dies wird erreicht, indem die characterData im Objekt options auf true gesetzt wird. Hier ist der Code:

 let options = { characterData: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'characterData') { // Do something here... } } }

Beachten Sie erneut, dass der type , nach dem in der Callback-Funktion gesucht wird characterData ist.

  • Siehe Live-Demo →

In diesem Beispiel suche ich nach Änderungen an einem bestimmten Textknoten, auf den ich über element.childNodes[0] . Das ist ein wenig hacky, aber es wird für dieses Beispiel reichen. Der Text kann vom Benutzer über das contenteditable -Attribut eines Absatzelements bearbeitet werden.

Herausforderungen beim Beobachten von Charakterdatenänderungen

Wenn Sie mit contenteditable herumgespielt haben, wissen Sie vielleicht, dass es Tastenkombinationen gibt, die eine Rich-Text-Bearbeitung ermöglichen. Beispielsweise macht STRG-B Text fett, STRG-I macht Text kursiv und so weiter. Dadurch wird der Textknoten in mehrere Textknoten aufgeteilt, sodass Sie feststellen werden, dass der MutationObserver nicht mehr reagiert, es sei denn, Sie bearbeiten den Text, der immer noch als Teil des ursprünglichen Knotens betrachtet wird.

Ich sollte auch darauf hinweisen, dass der MutationObserver den Rückruf nicht mehr auslöst, wenn Sie den gesamten Text löschen. Ich gehe davon aus, dass dies geschieht, weil das Zielelement nicht mehr vorhanden ist, sobald der Textknoten verschwindet. Um dem entgegenzuwirken, stoppt meine Demo die Beobachtung, wenn der Text entfernt wird, obwohl die Dinge ein wenig klebrig werden, wenn Sie Rich-Text-Shortcuts verwenden.

Aber keine Sorge, später in diesem Artikel werde ich einen besseren Weg besprechen, die Option characterData zu verwenden, ohne sich mit so vielen dieser Macken befassen zu müssen.

Beobachten von Änderungen an bestimmten Attributen

Zuvor habe ich Ihnen gezeigt, wie Sie Änderungen an Attributen eines bestimmten Elements beobachten können. In diesem Fall hätte ich, obwohl die Demo eine Änderung des Klassennamens auslöst, jedes Attribut des angegebenen Elements ändern können. Aber was ist, wenn ich Änderungen an einem oder mehreren bestimmten Attributen beobachten möchte, während ich die anderen ignoriere?

Ich kann das mit der optionalen Eigenschaft attributeFilter im option tun. Hier ist ein Beispiel:

 let options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'] }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }

Wie oben gezeigt, akzeptiert die Eigenschaft attributeFilter ein Array bestimmter Attribute, die ich überwachen möchte. In diesem Beispiel löst der MutationObserver den Rückruf jedes Mal aus, wenn eines oder mehrere der Attribute hidden , contenteditable oder data-par geändert wird.

  • Siehe Live-Demo →

Auch hier ziele ich auf ein bestimmtes Absatzelement ab. Beachten Sie das Auswahl-Dropdown, das auswählt, welches Attribut geändert wird. Das draggable Attribut ist das einzige, das sich nicht qualifiziert, da ich es in meinen Optionen nicht angegeben habe.

Beachten Sie im Code, dass ich erneut die Eigenschaft attributeName des MutationRecord -Objekts verwende, um zu protokollieren, welches Attribut geändert wurde. Und natürlich beginnt der MutationObserver wie bei den anderen Demos nicht mit der Überwachung auf Änderungen, bis auf die Schaltfläche „Start“ geklickt wird.

Eine andere Sache, die ich hier hervorheben sollte, ist, dass ich den attributes in diesem Fall nicht auf true setzen muss; Dies ist impliziert, da der attributesFilter auf „true“ gesetzt ist. Aus diesem Grund könnte mein Optionsobjekt wie folgt aussehen und genauso funktionieren:

 let options = { attributeFilter: ['hidden', 'contenteditable', 'data-par'] }

Wenn ich andererseits attributes zusammen mit einem attributeFilter -Array explizit auf false setze, würde dies nicht funktionieren, da der Wert false Vorrang hätte und die Filteroption ignoriert würde.

Beobachten von Änderungen an Knoten und ihrem Unterbaum

Bisher habe ich mich beim Einrichten jedes MutationObserver nur mit dem Zielelement selbst und im Fall von childList mit den unmittelbar untergeordneten Elementen des Elements befasst. Aber es könnte sicherlich einen Fall geben, in dem ich Änderungen an einem der folgenden Punkte beobachten möchte:

  • Ein Element und alle seine untergeordneten Elemente;
  • Ein oder mehrere Attribute für ein Element und seine untergeordneten Elemente;
  • Alle Textknoten innerhalb eines Elements.

All dies kann mit der subtree des Optionsobjekts erreicht werden.

childList Mit Teilbaum

Lassen Sie uns zunächst nach Änderungen an den untergeordneten Knoten eines Elements suchen, auch wenn es sich nicht um unmittelbar untergeordnete Knoten handelt. Ich kann mein Optionsobjekt so ändern, dass es so aussieht:

 options = { childList: true, subtree: true }

Alles andere im Code ist mehr oder weniger dasselbe wie im vorherigen childList Beispiel, zusammen mit einigen zusätzlichen Markups und Schaltflächen.

  • Siehe Live-Demo →

Hier gibt es zwei Listen, eine in der anderen verschachtelt. Wenn der MutationObserver gestartet wird, wird der Rückruf für Änderungen an einer der Listen ausgelöst. Aber wenn ich die subtree -Eigenschaft wieder auf false ändern würde (die Standardeinstellung, wenn sie nicht vorhanden ist), würde der Callback nicht ausgeführt werden, wenn die verschachtelte Liste geändert wird.

Attribute Mit Teilbaum

Hier ist ein weiteres Beispiel, dieses Mal mit einem subtree mit attributes und attributeFilter . Dadurch kann ich Änderungen an Attributen nicht nur am Zielelement, sondern auch an den Attributen aller untergeordneten Elemente des Zielelements beobachten:

 options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'], subtree: true }
  • Siehe Live-Demo →

Dies ähnelt der vorherigen Attribute-Demo, aber dieses Mal habe ich zwei verschiedene select-Elemente eingerichtet. Der erste ändert Attribute auf dem Ziel-Absatzelement, während der andere Attribute auf einem untergeordneten Element innerhalb des Absatzes ändert.

Auch hier gilt: Wenn Sie die subtree wieder auf „ false “ setzen (oder entfernen), würde die zweite Umschaltfläche den MutationObserver -Callback nicht auslösen. Und natürlich könnte ich attributeFilter ganz weglassen, und der MutationObserver würde nach Änderungen an irgendwelchen Attributen im Teilbaum statt an den angegebenen suchen.

characterData Mit Teilbaum

Denken Sie daran, dass es in der früheren characterData -Demo einige Probleme gab, bei denen der Zielknoten verschwand und der MutationObserver dann nicht mehr funktionierte. Es gibt zwar Möglichkeiten, dies zu umgehen, aber es ist einfacher, direkt auf ein Element als auf einen subtree zu zielen und dann die untergeordnete Baumeigenschaft zu verwenden, um anzugeben, dass alle Zeichendaten in diesem Element, egal wie tief es verschachtelt ist, ausgelöst werden sollen der MutationObserver -Callback.

Meine Optionen würden in diesem Fall so aussehen:

 options = { characterData: true, subtree: true }
  • Siehe Live-Demo →

Nachdem Sie den Beobachter gestartet haben, versuchen Sie, den bearbeitbaren Text mit STRG-B und STRG-I zu formatieren. Sie werden feststellen, dass dies viel effektiver funktioniert als das vorherige characterData Beispiel. In diesem Fall wirken sich die aufgelösten untergeordneten Knoten nicht auf den Beobachter aus, da wir alle Knoten innerhalb des Zielknotens beobachten und nicht nur einen einzelnen Textknoten.

Erfassung alter Werte

Wenn Sie Änderungen am DOM beobachten, möchten Sie häufig die alten Werte notieren und sie möglicherweise speichern oder an anderer Stelle verwenden. Dies kann mithilfe einiger verschiedener Eigenschaften im options erfolgen.

AttributAlterWert

Lassen Sie uns zunächst versuchen, den alten Attributwert abzumelden, nachdem er geändert wurde. So sehen meine Optionen zusammen mit meinem Rückruf aus:

 options = { attributes: true, attributeOldValue: true } function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
  • Siehe Live-Demo →

Beachten Sie die Verwendung der Eigenschaften attributeName und oldValue des MutationRecord -Objekts. Probieren Sie die Demo aus, indem Sie verschiedene Werte in das Textfeld eingeben. Beachten Sie, wie das Protokoll aktualisiert wird, um den zuvor gespeicherten Wert widerzuspiegeln.

ZeichenDatenAlterWert

Ähnlich sehen hier meine Optionen aus, wenn ich alte Charakterdaten protokollieren möchte:

 options = { characterData: true, subtree: true, characterDataOldValue: true }
  • Siehe Live-Demo →

Beachten Sie, dass die Protokollmeldungen den vorherigen Wert angeben. Die Dinge werden ein wenig wackelig, wenn Sie der Mischung HTML über Rich-Text-Befehle hinzufügen. Ich bin mir nicht sicher, was in diesem Fall das richtige Verhalten sein soll, aber es ist einfacher, wenn das einzige, was innerhalb des Elements ein einzelner Textknoten ist.

Abfangen von Mutationen mit takeRecords()

Eine weitere Methode des MutationObserver -Objekts, die ich noch nicht erwähnt habe, ist takeRecords() . Mit dieser Methode können Sie die erkannten Mutationen mehr oder weniger abfangen, bevor sie von der Callback-Funktion verarbeitet werden.

Ich kann diese Funktion mit einer Zeile wie dieser verwenden:

 let myRecords = observer.takeRecords();

Dies speichert eine Liste der DOM-Änderungen in der angegebenen Variablen. In meiner Demo führe ich diesen Befehl aus, sobald auf die Schaltfläche geklickt wird, die das DOM ändert. Beachten Sie, dass die Start- und Hinzufügen/Entfernen-Schaltflächen nichts protokollieren. Das liegt daran, dass ich, wie erwähnt, die DOM-Änderungen abfange, bevor sie vom Callback verarbeitet werden.

Beachten Sie jedoch, was ich im Ereignis-Listener mache, der den Beobachter stoppt:

 btnStop.addEventListener('click', function () { observer.disconnect(); if (myRecords) { console.log(`${myRecords[0].target} was changed using the ${myRecords[0].type} option.`); } }, false);

Wie Sie sehen können, greife ich nach dem Stoppen des Beobachters mit observer.disconnect() auf den abgefangenen Mutationsdatensatz zu und protokolliere das Zielelement sowie den Typ der aufgezeichneten Mutation. Wenn ich mehrere Arten von Änderungen beobachtet hätte, würde der gespeicherte Datensatz mehr als ein Element enthalten, jedes mit seinem eigenen Typ.

Wenn ein Mutationsdatensatz auf diese Weise durch Aufrufen von takeRecords() abgefangen wird, wird die Warteschlange der Mutationen geleert, die normalerweise an die Callback-Funktion gesendet würde. Wenn Sie also aus irgendeinem Grund diese Datensätze abfangen müssen, bevor sie verarbeitet werden, wäre takeRecords() praktisch.

Beobachten mehrerer Änderungen mit einem einzigen Beobachter

Beachten Sie, dass ich, wenn ich nach Mutationen auf zwei verschiedenen Knoten auf der Seite suche, denselben Beobachter verwenden kann. Das heißt, nachdem ich den Konstruktor aufgerufen habe, kann ich die Methode observe() für beliebig viele Elemente ausführen.

Also nach dieser Zeile:

 observer = new MutationObserver(mCallback);

Ich kann dann mehrere observe() Aufrufe mit verschiedenen Elementen als erstes Argument haben:

 observer.observe(mList, options); observer.observe(mList2, options);
  • Siehe Live-Demo →

Starten Sie den Beobachter und versuchen Sie es mit den Schaltflächen zum Hinzufügen/Entfernen für beide Listen. Der einzige Haken dabei ist, dass der Beobachter, wenn Sie auf eine der „Stopp“-Schaltflächen klicken, die Beobachtung für beide Listen beendet, nicht nur für die, auf die er abzielt.

Verschieben eines beobachteten Knotenbaums

Eine letzte Sache, auf die ich hinweisen möchte, ist, dass ein MutationObserver weiterhin Änderungen an einem bestimmten Knoten beobachtet, selbst nachdem dieser Knoten von seinem übergeordneten Element entfernt wurde.

Probieren Sie zum Beispiel die folgende Demo aus:

  • Siehe Live-Demo →

Dies ist ein weiteres Beispiel, das childList verwendet, um Änderungen an den untergeordneten Elementen eines childList zu überwachen. Beachten Sie die Schaltfläche, die die Unterliste trennt, die beobachtet wird. Klicken Sie auf die Schaltfläche „Start…“ und dann auf die Schaltfläche „Verschieben…“, um die verschachtelte Liste zu verschieben. Auch nachdem die Liste von ihrer übergeordneten Liste entfernt wurde, beobachtet der MutationObserver weiterhin die angegebenen Änderungen. Keine große Überraschung, dass dies passiert, aber es ist etwas, das man im Hinterkopf behalten sollte.

Fazit

Das deckt fast alle Hauptfunktionen der MutationObserver API ab. Ich hoffe, dieser Deep Dive war hilfreich, um sich mit diesem Standard vertraut zu machen. Wie bereits erwähnt, ist die Browserunterstützung stark und Sie können mehr über diese API auf den Seiten von MDN lesen.

Ich habe alle Demos für diesen Artikel in eine CodePen-Sammlung gepackt, falls Sie einen einfachen Ort haben möchten, an dem Sie mit den Demos herumspielen können.