Poznawanie interfejsu API MutationObserver

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ Monitorowanie zmian w DOM jest czasami potrzebne w złożonych aplikacjach internetowych i frameworkach. Za pomocą wyjaśnień wraz z interaktywnymi demonstracjami, ten artykuł pokaże Ci, w jaki sposób możesz użyć API MutationObserver, aby stosunkowo łatwo obserwować zmiany DOM.

W złożonych aplikacjach internetowych zmiany DOM mogą być częste. W rezultacie zdarzają się sytuacje, w których Twoja aplikacja może wymagać odpowiedzi na określoną zmianę w DOM.

Przez pewien czas akceptowanym sposobem wyszukiwania zmian w DOM była funkcja o nazwie Mutation Events, która jest obecnie przestarzała. Zatwierdzonym przez W3C zamiennikiem Mutation Events jest interfejs API MutationObserver, który będzie szczegółowo omawiać w tym artykule.

Wiele starszych artykułów i odnośników omawia, dlaczego stara funkcja została zastąpiona, więc nie będę tutaj omawiał tego szczegółowo (poza faktem, że nie byłbym w stanie zrobić tego sprawiedliwie). MutationObserver API ma prawie pełną obsługę przeglądarek, więc możemy go bezpiecznie używać w większości — jeśli nie we wszystkich — projektach, jeśli zajdzie taka potrzeba.

Podstawowa składnia dla MutationObserver

MutationObserver może być używany na wiele różnych sposobów, które omówię szczegółowo w dalszej części tego artykułu, ale podstawowa składnia MutationObserver wygląda tak:

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

Pierwsza linia tworzy nowy MutationObserver za pomocą konstruktora MutationObserver() . Argument przekazany do konstruktora to funkcja zwrotna, która będzie wywoływana przy każdej kwalifikującej się zmianie DOM.

Sposobem na określenie, co kwalifikuje się dla konkretnego obserwatora, jest ostatnia linijka w powyższym kodzie. W tym wierszu używam metody observe() MutationObserver , aby rozpocząć obserwację. Możesz to porównać do czegoś takiego jak addEventListener() . Jak tylko dołączysz detektor, strona będzie "nasłuchiwać" określonego zdarzenia. Podobnie, gdy zaczniesz obserwować, strona zacznie „obserwować” dla określonego MutationObserver .

Więcej po skoku! Kontynuuj czytanie poniżej ↓

Metoda observe() przyjmuje dwa argumenty: Target , który powinien być węzłem lub drzewem węzłów, na którym należy obserwować zmiany; oraz obiekt opcji , który jest obiektem MutationObserverInit , który umożliwia zdefiniowanie konfiguracji obserwatora.

Ostatnią kluczową podstawową cechą MutationObserver jest metoda disconnect() . To pozwala przestać obserwować określone zmiany i wygląda to tak:

 observer.disconnect();

Opcje konfiguracji MutationObserver

Jak wspomniano, metoda observe() obiektu MutationObserver wymaga drugiego argumentu, który określa opcje opisujące MutationObserver . Oto jak wyglądałby obiekt opcji z uwzględnieniem wszystkich możliwych par właściwość/wartość:

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

Podczas konfigurowania opcji MutationObserver nie jest konieczne uwzględnianie wszystkich tych wierszy. Włączam je tylko w celach informacyjnych, dzięki czemu możesz zobaczyć, jakie opcje są dostępne i jakie typy wartości mogą przyjmować. Jak widać, wszystkie oprócz jednego są logiczne.

Aby MutationObserver działał, co najmniej jeden childList , attributes , lub characterData musi być ustawiony na true , w przeciwnym razie zostanie zgłoszony błąd. Pozostałe cztery właściwości działają w połączeniu z jedną z tych trzech (więcej o tym później).

Do tej pory jedynie pomijałem składnię, aby dać ci przegląd. Najlepszym sposobem, aby zastanowić się, jak działa każda z tych funkcji, jest dostarczenie przykładów kodu i demonstracji na żywo, które zawierają różne opcje. Tak właśnie zrobię do końca tego artykułu.

Obserwowanie zmian w elementach podrzędnych za pomocą childList

Pierwszym i najprostszym MutationObserver , który można zainicjować, jest taki, który szuka węzłów podrzędnych określonego węzła (zwykle elementu), który ma zostać dodany lub usunięty. Na przykład zamierzam utworzyć nieuporządkowaną listę w moim kodzie HTML i chcę wiedzieć, kiedy węzeł podrzędny jest dodawany lub usuwany z tego elementu listy.

Kod HTML listy wygląda tak:

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

JavaScript dla mojego MutationObserver zawiera następujące elementy:

 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);

To tylko część kodu. Dla zwięzłości pokazuję najważniejsze sekcje, które dotyczą samego interfejsu API MutationObserver .

Zwróć uwagę, jak przechodzę przez argument mutations , który jest obiektem MutationRecord , który ma wiele różnych właściwości. W tym przypadku czytam właściwość type i rejestruję komunikat wskazujący, że przeglądarka wykryła mutację, która się kwalifikuje. Zauważ też, jak przekazuję element mList (odniesienie do mojej listy HTML) jako element docelowy (tj. element, na którym chcę obserwować zmiany).

  • Zobacz pełne interaktywne demo →

Użyj przycisków, aby uruchomić i zatrzymać MutationObserver . Komunikaty dziennika pomagają wyjaśnić, co się dzieje. Komentarze w kodzie również dostarczają wyjaśnienia.

Zwróć uwagę na kilka ważnych punktów:

  • Funkcja wywołania zwrotnego (którą nazwałem mCallback , aby zilustrować, że możesz nazwać ją, jak chcesz) zostanie uruchomiona za każdym razem, gdy wykryta zostanie udana mutacja i po wykonaniu metody Observe observe() .
  • W moim przykładzie jedynym „typem” mutacji, który się kwalifikuje, jest childList , więc warto szukać tego podczas przeglądania MutationRecord. Szukanie jakiegokolwiek innego typu w tym przypadku nic by nie dało (inne typy będą używane w kolejnych demonstracjach).
  • Używając childList , mogę dodać lub usunąć węzeł tekstowy z elementu docelowego i to też by się kwalifikowało. Więc nie musi to być element dodany lub usunięty.
  • W tym przykładzie kwalifikują się tylko bezpośrednie węzły podrzędne. W dalszej części artykułu pokażę, jak można to zastosować do wszystkich węzłów podrzędnych, wnuków i tak dalej.

Obserwacja zmian atrybutów elementu

Innym powszechnym typem mutacji, który możesz chcieć śledzić, jest zmiana atrybutu określonego elementu. W następnym interaktywnym demo będę obserwować zmiany w atrybutach elementu akapitu.

 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);
  • Wypróbuj demo →

Ponownie skróciłem kod dla jasności, ale ważne części to:

  • Obiekt options używa właściwości attributes , ustawionej na wartość true aby poinformować MutationObserver , że chcę szukać zmian w atrybutach elementu docelowego.
  • Typ mutacji, który testuję w mojej pętli, to attributes , jedyny, który kwalifikuje się w tym przypadku.
  • Używam również właściwości attributeName obiektu mutation , która pozwala mi dowiedzieć się, który atrybut został zmieniony.
  • Kiedy uruchamiam obserwatora, przekazuję element akapitu przez odwołanie, wraz z opcjami.

W tym przykładzie przycisk służy do przełączania nazwy klasy na docelowym elemencie HTML. Funkcja wywołania zwrotnego w obserwatorze mutacji jest wyzwalana za każdym razem, gdy klasa jest dodawana lub usuwana.

Obserwacja zmian danych postaci

Kolejną zmianą, której możesz chcieć poszukać w swojej aplikacji, są mutacje danych znakowych; czyli zmiany w określonym węźle tekstowym. Odbywa się to poprzez ustawienie właściwości characterData na true w obiekcie options . Oto kod:

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

Zwróć uwagę, że type , którego szukamy w funkcji zwrotnej, to characterData .

  • Zobacz demo na żywo →

W tym przykładzie szukam zmian w określonym węźle tekstowym, do którego kieruję poprzez element.childNodes[0] . To trochę zwariowane, ale wystarczy w tym przykładzie. Tekst może być edytowany przez użytkownika za pomocą atrybutu contenteditable w elemencie akapitu.

Wyzwania podczas obserwowania zmian danych postaci

Jeśli masz do czynienia z contenteditable , być może wiesz, że istnieją skróty klawiaturowe umożliwiające edycję tekstu sformatowanego. Na przykład CTRL-B powoduje pogrubienie tekstu, CTRL-I zmienia tekst na kursywę i tak dalej. Spowoduje to rozbicie węzła tekstowego na wiele węzłów tekstowych, więc zauważysz, że MutationObserver przestanie odpowiadać, chyba że zmodyfikujesz tekst, który nadal jest uważany za część oryginalnego węzła.

Powinienem również zaznaczyć, że jeśli usuniesz cały tekst, MutationObserver nie będzie już wyzwalał wywołania zwrotnego. Zakładam, że tak się dzieje, ponieważ po zniknięciu węzła tekstowego element docelowy już nie istnieje. Aby temu zaradzić, moje demo przestaje obserwować po usunięciu tekstu, chociaż rzeczy stają się trochę lepkie, gdy używasz skrótów z tekstem sformatowanym.

Ale nie martw się, w dalszej części tego artykułu omówię lepszy sposób korzystania z opcji characterData bez konieczności zajmowania się tyloma z tych dziwactw.

Obserwacja zmian w określonych atrybutach

Wcześniej pokazałem, jak obserwować zmiany atrybutów na określonym elemencie. W takim przypadku, mimo że demo wyzwala zmianę nazwy klasy, mogłem zmienić dowolny atrybut określonego elementu. Ale co, jeśli chcę obserwować zmiany jednego lub więcej określonych atrybutów, ignorując inne?

Mogę to zrobić za pomocą opcjonalnej właściwości attributeFilter w obiekcie option . Oto przykład:

 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... } } }

Jak pokazano powyżej, właściwość attributeFilter akceptuje tablicę określonych atrybutów, które chcę monitorować. W tym przykładzie MutationObserver wyzwoli wywołanie zwrotne za każdym razem, gdy jeden lub więcej atrybutów hidden , contenteditable lub data-par zostanie zmodyfikowany .

  • Zobacz demo na żywo →

Ponownie celuję w konkretny element akapitu. Zwróć uwagę na listę rozwijaną wyboru, która określa, który atrybut zostanie zmieniony. Atrybut draggable jest jedynym, który się nie kwalifikuje, ponieważ nie określiłem go w moich opcjach.

Zauważ w kodzie, że ponownie używam właściwości attributeName obiektu MutationRecord do rejestrowania, który atrybut został zmieniony. I oczywiście, podobnie jak w przypadku innych wersji demonstracyjnych, MutationObserver nie rozpocznie monitorowania zmian, dopóki nie zostanie kliknięty przycisk „start”.

Jeszcze jedną rzeczą, na którą powinienem zwrócić uwagę, jest to, że w tym przypadku nie muszę ustawiać wartości attributes na true ; jest to dorozumiane, ponieważ attributesFilter jest ustawiona na wartość true. Dlatego mój obiekt opcji mógłby wyglądać następująco i działałby tak samo:

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

Z drugiej strony, jeśli jawnie ustawię attributes na false wraz z tablicą attributeFilter , to nie zadziała, ponieważ wartość false miałaby pierwszeństwo, a opcja filtra zostałaby zignorowana.

Obserwacja zmian w węzłach i ich poddrzewie

Do tej pory podczas konfigurowania każdego MutationObserver , miałem do czynienia tylko z samym elementem docelowym i, w przypadku childList , bezpośrednimi dziećmi elementu. Ale z pewnością może zaistnieć przypadek, w którym chciałbym zaobserwować zmiany w jednym z następujących:

  • Element i wszystkie jego elementy podrzędne;
  • Jeden lub więcej atrybutów elementu i jego elementów podrzędnych;
  • Wszystkie węzły tekstowe wewnątrz elementu.

Wszystkie powyższe można osiągnąć za pomocą właściwości subtree obiektu options.

Lista dzieci Z poddrzewem

Najpierw poszukajmy zmian w węzłach podrzędnych elementu, nawet jeśli nie są one bezpośrednimi dziećmi. Mogę zmienić mój obiekt opcji, aby wyglądał tak:

 options = { childList: true, subtree: true }

Wszystko inne w kodzie jest mniej więcej takie samo jak w poprzednim przykładzie childList , wraz z dodatkowymi znacznikami i przyciskami.

  • Zobacz demo na żywo →

Tutaj są dwie listy, jedna zagnieżdżona w drugiej. Po uruchomieniu MutationObserver wywołanie zwrotne zostanie wywołane w przypadku zmian na dowolnej z list. Ale gdybym miał zmienić właściwość subtree z powrotem na false (domyślnie, gdy jej nie ma), wywołanie zwrotne nie zostałoby wykonane, gdy zagnieżdżona lista zostanie zmodyfikowana.

Atrybuty Z poddrzewem

Oto kolejny przykład, tym razem używający subtree z attributes i attributeFilter . To pozwala mi obserwować zmiany atrybutów nie tylko w elemencie docelowym, ale także w atrybutach dowolnych elementów podrzędnych elementu docelowego:

 options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'], subtree: true }
  • Zobacz demo na żywo →

Jest to podobne do poprzedniego demo atrybutów, ale tym razem ustawiłem dwa różne elementy wyboru. Pierwszy modyfikuje atrybuty w docelowym elemencie akapitu, podczas gdy drugi modyfikuje atrybuty elementu podrzędnego wewnątrz akapitu.

Ponownie, jeśli ustawisz opcję subtree z powrotem na wartość false (lub ją usuniesz), drugi przycisk przełączania nie wywoła wywołania zwrotnego MutationObserver . I oczywiście mógłbym całkowicie pominąć attributeFilter , a MutationObserver zmian w dowolnych atrybutach w poddrzewie, a nie w określonych.

charakterDane Z poddrzewem

Pamiętaj, że we wcześniejszej wersji demonstracyjnej characterData wystąpiły pewne problemy ze znikaniem docelowego węzła, a następnie MutationObserver . Chociaż istnieją sposoby na obejście tego, łatwiej jest kierować element bezpośrednio niż węzeł tekstowy, a następnie użyć właściwości subtree , aby określić, że chcę, aby wszystkie dane znakowe wewnątrz tego elementu, bez względu na to, jak głęboko są zagnieżdżone, były wyzwalane wywołanie zwrotne MutationObserver .

Moje opcje w tym przypadku wyglądałyby tak:

 options = { characterData: true, subtree: true }
  • Zobacz demo na żywo →

Po uruchomieniu obserwatora spróbuj użyć klawiszy CTRL-B i CTRL-I, aby sformatować tekst do edycji. Zauważysz, że działa to znacznie efektywniej niż poprzedni przykład characterData . W tym przypadku podzielone węzły podrzędne nie wpływają na obserwatora, ponieważ obserwujemy wszystkie węzły wewnątrz węzła docelowego, zamiast pojedynczego węzła tekstowego.

Zapisywanie starych wartości

Często, obserwując zmiany w DOM, będziesz chciał zanotować stare wartości i ewentualnie przechowywać je lub używać w innym miejscu. Można to zrobić za pomocą kilku różnych właściwości w obiekcie options .

atrybutStaraWartość

Najpierw spróbujmy wylogować starą wartość atrybutu po jej zmianie. Oto jak będą wyglądać moje opcje wraz z wywołaniem zwrotnym:

 options = { attributes: true, attributeOldValue: true } function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
  • Zobacz demo na żywo →

Zwróć uwagę na użycie właściwości attributeName i oldValue obiektu MutationRecord . Wypróbuj demo, wprowadzając różne wartości w polu tekstowym. Zwróć uwagę, jak aktualizuje się dziennik, aby odzwierciedlić poprzednią zapisaną wartość.

postaćDaneStaraWartość

Podobnie, oto jak wyglądałyby moje opcje, gdybym chciał logować stare dane znaków:

 options = { characterData: true, subtree: true, characterDataOldValue: true }
  • Zobacz demo na żywo →

Zauważ, że komunikaty dziennika wskazują poprzednią wartość. Sprawy stają się trochę niepewne, gdy dodajesz do miksu kod HTML za pomocą poleceń tekstu sformatowanego. Nie jestem pewien, jakie powinno być prawidłowe zachowanie w tym przypadku, ale jest to prostsze, jeśli jedyną rzeczą wewnątrz elementu jest pojedynczy węzeł tekstowy.

Przechwytywanie mutacji za pomocą takeRecords()

Inną metodą obiektu MutationObserver , o której jeszcze nie wspomniałem, jest takeRecords() . Ta metoda pozwala mniej lub bardziej przechwycić mutacje wykryte przed ich przetworzeniem przez funkcję zwrotną.

Mogę użyć tej funkcji za pomocą linii takiej jak ta:

 let myRecords = observer.takeRecords();

To przechowuje listę zmian DOM w określonej zmiennej. W moim demo wykonuję to polecenie zaraz po kliknięciu przycisku modyfikującego DOM. Zauważ, że przyciski start i dodaj/usuń niczego nie rejestrują. Dzieje się tak, ponieważ, jak wspomniano, przechwytuję zmiany DOM, zanim zostaną przetworzone przez wywołanie zwrotne.

Zwróć jednak uwagę, co robię w odbiorniku zdarzeń, który zatrzymuje obserwatora:

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

Jak widać, po zatrzymaniu obserwatora za pomocą observer.disconnect() , uzyskuję dostęp do rekordu mutacji, który został przechwycony i loguję element docelowy oraz rodzaj zarejestrowanej mutacji. Gdybym obserwował wiele typów zmian, przechowywany rekord zawierałby więcej niż jeden element, każdy z własnym typem.

Gdy rekord mutacji zostanie przechwycony w ten sposób przez wywołanie takeRecords() , kolejka mutacji, która normalnie zostałaby wysłana do funkcji zwrotnej, zostaje opróżniona. Więc jeśli z jakiegoś powodu musisz przechwycić te rekordy przed ich przetworzeniem, przyda się takeRecords() .

Obserwacja wielu zmian za pomocą jednego obserwatora

Zauważ, że jeśli szukam mutacji na dwóch różnych węzłach na stronie, mogę to zrobić przy użyciu tego samego obserwatora. Oznacza to, że po wywołaniu konstruktora mogę wykonać metodę observe() dla tylu elementów, ile chcę.

Tak więc po tej linii:

 observer = new MutationObserver(mCallback);

Mogę wtedy mieć wiele wywołań observe() z różnymi elementami jako pierwszy argument:

 observer.observe(mList, options); observer.observe(mList2, options);
  • Zobacz demo na żywo →

Uruchom obserwatora, a następnie wypróbuj przyciski dodawania/usuwania dla obu list. Jedynym haczykiem jest to, że jeśli naciśniesz jeden z przycisków „stop”, obserwator przestanie obserwować obie listy, a nie tylko tę, na którą kieruje.

Przenoszenie obserwowanego drzewa węzłów

Ostatnią rzeczą, na którą wskażę, jest to, że MutationObserver będzie nadal obserwować zmiany w określonym węźle, nawet po usunięciu tego węzła z elementu nadrzędnego.

Na przykład wypróbuj następujące demo:

  • Zobacz demo na żywo →

To jest kolejny przykład, który używa childList do monitorowania zmian w elementach podrzędnych elementu docelowego. Zwróć uwagę na przycisk, który odłącza podlistę, która jest obserwowana. Kliknij przycisk "Start ...", a następnie kliknij przycisk "Przenieś ...", aby przenieść zagnieżdżoną listę. Nawet po usunięciu listy z rodzica MutationObserver nadal obserwuje określone zmiany. Nie jest to wielka niespodzianka, że ​​tak się dzieje, ale należy o tym pamiętać.

Wniosek

Obejmuje to prawie wszystkie podstawowe funkcje interfejsu API MutationObserver . Mam nadzieję, że to głębokie nurkowanie było przydatne do zapoznania się z tym standardem. Jak wspomniano, obsługa przeglądarek jest silna i możesz przeczytać więcej o tym interfejsie API na stronach MDN.

Wszystkie wersje demonstracyjne tego artykułu umieściłem w kolekcji CodePen, jeśli chcesz mieć łatwe miejsce do zabawy z demami.