Kennenlernen der MutationObserver-API
Veröffentlicht: 2022-03-10In 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
.
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 Observeobserve()
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 dieattributes
-Eigenschaft, die auftrue
gesetzt ist, um demMutationObserver
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
desmutation
, 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.