Podstawy#
Obserwatorzy zmian#
Jednymi z najczęściej wykonywanych czynności w obszarze DOM jest po prostu modyfikowanie węzłów w drzewie węzłów. Obejmuje to dodawanie/usuwanie węzłów, jak również zmianę ich poszczególnych cech (np. danych tekstowych czy wartości atrybutów w elementach). W niektórych przypadkach niezwykle przydatna okazałaby się możliwość zasygnalizowania takich zmian i ewentualnego ich przetworzenia.
Na przestrzeni lat poszczególne specyfikacje DOM wprowadziły dwa rozwiązania oparte na różnych mechanizmach:
W chwili obecnej jedynym słusznym wyborem pozostają obserwatorzy zmian i to właśnie im poświęcę najwięcej uwagi. Zdarzenia zmian, mimo dużej popularności i krótkiemu zachwytowi, okazały się wybitnie nietrafionym pomysłem i niewykluczone, że w niedalekiej przyszłości zostaną całkowicie usunięte z przeglądarek internetowych.
Rozwiązanie nowoczesne (obiekty typu MutationObserver)#
DOM4 wprowadza nowy mechanizm w postaci obserwatorów zmian, który jest odpowiednikiem dla zdarzeń zmian, ale działającym zdecydowanie wydajniej od "pierwowzoru". Z obserwatorami zmian skojarzonych będzie kilka innych definicji, chociaż nimi zajmiemy się w dalszej części kursu.
Prosty przykład:
<script>
// Tworzymy nowego obserwatora 'newObserver'
var newObserver = new MutationObserver(function(records, observer){
// W czasie wywołania funkcji zwrotnej mamy dostęp do:
// records - lista ze wszystkimi obiektami MutationRecord
// observer - nasz newObserwer
var getInfo = "";
var recordsLen = records.length;
for (var i = 0; i < recordsLen; i++){
var record = records[i];
getInfo += "<br><br>" + "records[" + i + "]: " + record
+ "<br>" + "record.type: " + record.type
+ "<br>" + "record.target: " + record.target
+ "<br>" + "record.addedNodes.length: " + record.addedNodes.length
+ "<br>" + "record.attributeName: " + record.attributeName
+ "<br>" + "record.attributeNamespace: " + record.attributeNamespace
+ "<br>" + "record.oldValue: " + record.oldValue;
}
document.documentElement.innerHTML += "Liczba zmian records.length: " + recordsLen
+ "<br><br>" + "Odczytujemy poszczególne dane każdej zmiany:"
+ getInfo;
});
// Tworzymy obiekt konfiguracyjny
var config = {
childList: true,
attributes: true,
attributeOldValue: true,
attributeFilter: ["id", "name"],
subtree: true,
characterDataOldValue: true
};
var new_P = document.createElement("P");
newObserver.observe(new_P, config); // rejestrujemy 'newObserver' na węźle 'new_P'
// Przeprowadzamy różne zmiany na węźle 'new_P' i jego zawartości
new_P.id = "id1";
new_P.id = "id2";
new_P.id = "id2";
new_P.className = "important"; // ten atrybut nie podlega obserwacji
new_P.appendChild(document.createElement("span"));
new_P.textContent = "Start";
new_P.childNodes[0].textContent = "Witam1";
new_P.childNodes[0].textContent = "Witam2";
new_P.childNodes[0].textContent = "Witam2";
</script>
Dla lepszej orientacji najlepiej już teraz scharakteryzować całość w kilku uproszczonych krokach:
- Tworzymy konstruktorem
new MutationObserver()
nowy obserwator zmiannewObserver
, z jednoczesnym ustawieniem funkcji zwrotnej, która będzie automatycznie wywoływana przy każdej zapisanej zmianie. - Definiujemy obiekt konfiguracyjny
config
, który określa rodzaje zapisywanych zmian. - Podczepiamy obserwatora zmian
newObserver
do konkretnego węzła za pomocą metodyMutationObserver.observe()
, która przyjmuje referencję do obserwowanego węzła oraz obiekt konfiguracyjnyconfig
. - Po każdej zapisanej zmianie wywołana zostanie funkcja zwrotna skojarzona z obserwatorem zmian
newObserver
, w której dostępna będzie lista z obiektami typuMutationRecorder
, które przechowują konkretne informacje o dokonanej zmianie.
Śledzenie zmian za pomocą obserwatorów zmian jest o tyle ciekawsze, że za jednym zamachem można zapisać wiele rodzajów zmian, nie tylko dla węzła, na którym obserwator zmian został zarejestrowany, ale z możliwością uwzględnienia wszystkich jego potomków. Odpada konieczność odpalania i przechwytywania masy zdarzeń (a także ich stałej propagacji), które towarzyszyły przy każdej najdrobniejszej zmianie w poprzedniej metodzie, czyniąc ją przez to bardzo nieefektywną. Teraz wywoływana jest pojedyncza funkcja zwrotna, bez całego narzutu związanego z przepływem zdarzeń.
Podejście algorytmiczne#
Pierwszy kontakt z pojęciami i algorytmami w przypadku obserwatorów zmian możne nieco odstraszać. Moim skromnym zdaniem całość jest na tyle przydatna i jednocześnie ciekawa z perspektywy algorytmiki, że postanowiłem utworzyć krótkie streszczenie tego procesu. Przed rozpoczęciem analizy właściwych algorytmów najlepiej zapoznać się z poniższym wprowadzeniem.
Cały mechanizm stosowania obserwatorów zmian można wyrazić w trzech etapach:
- Etap 1. Tworzenie i rejestrowanie obserwatorów zmian #
Utworzenie nowego obserwatora zmian konstruktorem
new MutationObserver()
automatycznie doda go do specjalnej listy obserwatorów zmian skojarzonej z jednostką powiązanych podobnych względem pochodzenia kontekstów przeglądania (zgodnie z HTML5). W tym momencie nie ma znaczenia czy nowy obserwator zmian został zarejestrowany czy też nie, do wspomnianej listy trafiają wszyscy obserwatorzy zmian. Nowy obserwator zmian będzie również kojarzony z funkcją zwrotną, którą przekazujemy jako pierwszy argument do konstruktora.Następnie przy użyciu metody
MutationObserver.observe()
rejestrujemy naszego nowego obserwatora zmian na konkretnym węźle (pierwszy argumenty) i ustalamy odpowiednie kryteria obserwacji (drugi argument).- Etap 2. Tworzenie zapisów zmian #
Algorytmy definiowane przez DOM (i inne specyfikacje też), które w jakikolwiek sposób modyfikują węzły (wliczając w to atrybuty jeśli są skojarzone z elementami), dają znać, że ta konkretna zmiana musi być zapisana. Odbywa się to poprzez wywołanie w pewnym momencie algorytmu zakolejkowania zapisu zmian z przekazaniem kilku istotnych informacji (np. typ zmiany, cel zmiany i cała reszta).
Oto oryginalny fragment jednego z kroków w algorytmie modyfikującym atrybuty:
Queue a mutation record of "attributes" for element with name attribute's local name, namespace attribute's namespace, and oldValue attribute's value.
Algorytm zakolejkowania zapisu zmian pobiera najpierw wszystkich zarejestrowanych obserwatorów, zaczynając od przekazanego celu zmiany (z uwzględnieniem wszystkich jego przodków), których parametry obserwacji (ustalane przy ich rejestrowaniu w etapie 1) pasują do informacji przekazanych do algorytmu zakolejkowania zapisu zmian, i umieszcza ich w specjalnej liście pasujących obserwatorów.
Uwzględnianie zarejestrowanych obserwatorów w przodkach celu zmiany wynika z tego, że ci obserwatorzy mogą mieć ustawioną opcję
subtree
w obiekcie konfiguracyjnym, która nakazuje śledzenie zmian we wszystkich swoich potomkach (do których zalicza się przekazany cel zmiany).Następnie dla każdego obserwatora zmian z listy pasujących obserwatorów tworzony jest zapis zmian (czyli obiekt implementujący interfejs MutationRecord), ustawiane są jego poszczególne cechy opisujące zmianę, i na koniec obiekt ten dodawany jest do kolejki zapisu skojarzonej z danym obserwatorem zmian.
Zwieńczeniem tego etapu będzie wywołanie algorytmu zakolejkowania złożonego zadania obserwatora zmian (przejście do etapu 3).
- Etap 3. Zgłaszanie obserwatorów zmian #
W tym momencie zachodzą najciekawsze rzeczy. Zgłoszenie zakolejkowania złożonego zadania obserwatora zmian (z etapu 2) najpierw analizuje czy flaga kolejkowania złożonego mikrozadania obserwatora zmian była ustawiona czy też nie. Jeśli była już ustawiona to nie robi niczego, w przeciwnym razie ustawia ją i dodaje do kolejki zadań HTML5 nowe zadanie w postaci algorytmu zgłoszenia obserwatorów zmian.
Operowanie flagą jest tutaj kluczowe, bo dzięki niej można wykonać wiele zapisów zmian (tworzonych w etapie 2), ale wszystkie one mogą zostać uwzględnione w pojedynczym zgłoszeniu obserwatorów zmian (przykład praktyczny).
Można powiedzieć, że zgłoszenie obserwatorów zmian, które trafia do kolejki zadań HTML5 jest tylko żądaniem (ma charakter asynchroniczny). Zanim faktycznie zostanie zrealizowane może minąć nieco czasu (w zależności od zajętości tej kolejki). Kiedy jednak proces się rozpocznie, to pierwszym jego krokiem jest dezaktywacja flagi kolejkowania złożonego mikrozadania obserwatora zmian), dzięki czemu kolejne utworzenie zapisu zmian z etapu 2 doda do kolejki HTML5 nowe zgłoszenie obserwatorów zmian. Przed dezaktywacją flagi wszystkie zapisy zmian utworzone do tej pory będą objęte jednym zgłoszeniem lokowanym w kolejce HTML5 i żadne nowe zgłoszenia nie będą dodawane do kolejki zadań HTML5.
Dalej sytuacja jest już prosta. Analizowani są wszyscy obserwatorzy zmian znajdujący się w liście obserwatorów zmian skojarzonej z jednostką powiązanych podobnych względem pochodzenia kontekstów przeglądania (patrz etap 1). Dla tych obserwatorów zmian, których kolejka zapisu nie jest pusta (będzie zapełniana w etapie 2), wywoływana jest ich funkcja zwrotna (ustalana w etapie 1). Sama kolejka zapisu jest czyszczona, a to co ewentualnie możemy przekazać do funkcji zwrotnej i sobie przeanalizować (pierwszy argument) jest tylko jej kopią (przykład praktyczny).
Automatyczne czyszczenie kolejki zapisu w czasie jej odczytu jest istotne z perspektywy oszczędności pamięci. Bez tego procesu czyszczenie należałoby wykonywać ręcznie, co z pewnością byłoby pomijane przez wielu programistów i doprowadzało do nieustannych wycieków pamięci.
Kopię kolejki zapisu (z jednoczesnym jej wyczyszczeniem) można pobrać samodzielnie za pomocą metody
MutationObserver.takeRecords()
. Jeśli wywołamy metodę zanim zadanie zgłoszenia obserwatorów zmian z kolejki zadań HTML5 faktycznie zostanie rozpoczęte, to doprowadzimy do sytuacji, w której funkcja zwrotna tego obserwatora zmian nie będzie wywoływana ze względu na pustą kolejkę zapisu (przykład praktyczny).
Studium przypadków#
Teoria teorią, ale nic bardziej nie trafia do świadomości większości osób niż garść przykładów praktycznych. Przeanalizujmy kilka najciekawszych i najistotniejszych przypadków.
Kolejność tworzenia obserwatorów zmian a wywoływanie funkcji zwrotnych#
Jeśli kilku obserwatorów zmian śledzi ten sam rodzaj zmian dla danego węzła, to kolejność wywoływania skojarzonych z nimi funkcji zwrotnych zależy od kolejności utworzenia tych obserwatorów. Kolejność podczepiania danego obserwatora zmian do konkretnego węzła metodą MutationObserver.observe()
nie ma znaczenia (zgodnie z etapem 1).
Prosty przykład:
<script>
// Tworzymy nowego obserwatora 'newObserver1'
var newObserver1 = new MutationObserver(function(){
document.documentElement.innerHTML += "newObserver1" + "<br>";
});
// Tworzymy kolejnego obserwatora 'newObserver2'
var newObserver2 = new MutationObserver(function(){
document.documentElement.innerHTML += "newObserver2" + "<br>";
});
// Tworzymy kolejnego obserwatora 'newObserver3'
var newObserver3 = new MutationObserver(function(){
document.documentElement.innerHTML += "newObserver3" + "<br>";
});
// Tworzymy ten sam obiekt konfiguracyjny dla wszystkich obserwatorów
var config = {
attributes: true
};
var new_P = document.createElement("P");
newObserver3.observe(new_P, {attributes: true}); // rejestrujemy 'newObserver3' na węźle 'new_P'
newObserver2.observe(new_P, {attributes: true}); // rejestrujemy 'newObserver2' na węźle 'new_P'
newObserver1.observe(new_P, {attributes: true}); // rejestrujemy 'newObserver1' na węźle 'new_P'
// Modyfikujemy atrybut ID w węźle 'new_P'
new_P.id = "1";
</script>
Pojedyncza rejestracja tego samego obserwatora zmian dla węzła#
Jeśli dany obserwator zmian zostanie kilkukrotnie zarejestrowany metodą MutationObserver.observe()
na tym samym węźle, to pod uwagę brane jest ostatnie wywołanie rejestrującej metody. Oczywiście można rejestrować danego obserwatora zmian na dowolnej liczbie węzłów.
Prosty przykład:
<script>
// Tworzymy nowego obserwatora 'newObserver'
var newObserver = new MutationObserver(function(records, observer){
document.documentElement.innerHTML += "Nowe zgłoszenie obserwatorów zmian"
+ "<br>" + "Liczba zmian records.length: " + records.length
+ "<br>" + "Rodzaj pierwszej zmiany records[0].type: " + records[0].type
});
var new_P = document.createElement("P");
newObserver.observe(new_P, {childList: true});
newObserver.observe(new_P, {characterDataOldValue: true});
newObserver.observe(new_P, {attributes: true}); // tylko ten wariant będzie uwzględniany
new_P.id = "setID";
</script>
Wiele zapisów zmian i jedno zgłoszenie obserwatorów zmian#
Możliwe jest utworzenie wielu zapisów zmian zanim faktycznie rozpocznie się przetwarzania nowego zadania w postaci algorytmu zgłoszenia obserwatorów zmian, które dodawane jest do kolejki zadań HTML5 (zgodnie z etapem 2 i etapem 3). Chociaż zmiany rejestrowane są na bieżąco (synchronicznie), to w rzeczywistości ich odczyt nastąpi zawsze z pewnym opóźnieniem (asynchronicznie), które zależeć będzie od zajętości kolejki zadań HTML5. W przypadku wielu operacji DOM jedna po drugiej zazwyczaj będziemy mieli do czynienia z pojedynczym zgłoszeniem, ale na upartego można samodzielnie wymusić opóźnienie, np. poprzez funkcję czasową.
Prosty przykład:
<script>
// Tworzymy nowego obserwatora 'newObserver'
var newObserver = new MutationObserver(function(records, observer){
document.documentElement.innerHTML += "Nowe zgłoszenie obserwatorów zmian"
+ "<br>" + "Liczba zmian records.length: " + records.length + "<br><br>";
});
var new_P = document.createElement("P");
newObserver.observe(new_P, {attributes: true});
// Pierwsze zgłoszenie obejmie 3 zmiany
new_P.id = "setID";
new_P.id = "setID";
new_P.id = "setID";
// Drugie zgłoszenie obejmie 1 zmianę
setTimeout(function(){new_P.id = "setID";}, 1000);
</script>
Samodzielne pobranie kolejki zapisu blokuje wywołanie funkcji zwrotnej#
Za pomocą metody MutationObserver.takeRecords()
możemy samodzielnie pobrać kolejkę zapisu (a raczej jej kopię), przez co funkcja zwrotna nie będzie wywoływana (zgodnie z etapem 3).
Prosty przykład:
<script>
// Tworzymy nowego obserwatora 'newObserver'
var newObserver = new MutationObserver(function(records, observer){
// Funkcja zwrotna nie zostanie wywołana
document.documentElement.innerHTML += "Nowe zgłoszenie obserwatorów zmian"
+ "<br>" + "Liczba zmian records.length: " + records.length + "<br><br>";
});
var new_P = document.createElement("P");
newObserver.observe(new_P, {attributes: true});
new_P.id = "setID";
new_P.id = "setID";
new_P.id = "setID";
document.write(newObserver.takeRecords().length); // 3 - kolejka zapisu została wyczyszczona jeszcze przed wywołaniem funkcji zwrotnej
document.write("<br>");
document.write(newObserver.takeRecords().length); // 0 - w tym miejscu kolejka zapisu była już pusta
</script>
Podobnie w przypadku odłączenia (odrejestrowania) danego obserwatora zmian ze wszystkich obserwowanych przez niego węzłów przy użyciu metody MutationObserver.disconnect()
. Ona również wyczyści kolejkę zapisu, przez co funkcja zwrotna nie będzie wywoływana.
Prosty przykład:
<script>
// Tworzymy nowego obserwatora 'newObserver'
var newObserver = new MutationObserver(function(records, observer){
// Funkcja zwrotna nie zostanie wywołana
document.documentElement.innerHTML += "Nowe zgłoszenie obserwatorów zmian"
+ "<br>" + "Liczba zmian records.length: " + records.length + "<br><br>";
});
var new_P = document.createElement("P");
newObserver.observe(new_P, {attributes: true});
new_P.id = "setID";
new_P.id = "setID";
new_P.id = "setID";
newObserver.disconnect();
document.write(newObserver.takeRecords().length); // 0 - poprzednia metoda wyczyściła kolejkę zapisu
</script>
Automatyczne czyszczenie kolejki zapisu przed wywołaniem funkcji zwrotnej#
Kiedy zgłoszenie obserwatorów zmian dodane do kolejki zadań HTML5 zacznie być faktycznie przetwarzane, to przed wywołaniem funkcji zwrotnej danego obserwatora zmian jego kolejka zapisu zostaje samoczynnie wyczyszczona (zgodnie z etapem 3). My będziemy mieli dostęp do jej kopii (jeszcze sprzed wyczyszczenia) dzięki pierwszemu argumentowi przekazywanemu do funkcji zwrotnej. Ciekawe jest to, że możemy dokonywać zmian na obserwowanym węźle bezpośrednio w funkcji zwrotnej, i tym samym zmodyfikujemy jego kolejkę zapisu ponownie, ale odbywa się to po jej wcześniejszym skopiowaniu i wyczyszczeniu.
Prosty przykład:
<script>
// Tworzymy nowego obserwatora 'newObserver'
var newObserver = new MutationObserver(function(records, observer){
new_P.id = "setID"; // robimy zmianę wprost w funkcji zwrotnej
var takeRecordsAgain1 = observer.takeRecords().length; // 1
var takeRecordsAgain2 = newObserver.takeRecords().length; // 0
document.documentElement.innerHTML += "Lista zmian wykonanych poza funkcją zwrotną:"
+ "<br>" + "records.length: " + records.length
+ "<br><br>" + "Lista zmian wykonanych wewnątrz funkcji zwrotnej:"
+ "<br>" + "observer.takeRecords().length (pierwsze pobranie): " + takeRecordsAgain1
+ "<br>" + "newObserver.takeRecords().length (drugie pobranie): " + takeRecordsAgain2;
});
var new_P = document.createElement("P");
newObserver.observe(new_P, {attributes: true});
new_P.id = "setID";
new_P.id = "setID";
new_P.id = "setID";
</script>
Powyższy przykład jest interesujący z jeszcze jednego względu. Ponowne ustawienie identyfikatora akapitu bezpośrednio w funkcji zwrotnej powoduje utworzenie nowego zapisu zmian i w konsekwencji dodanie do kolejki zadań HTML5 nowego zgłoszenie obserwatorów zmian (bo flaga blokująca była w tym momencie nieaktywna). Niemniej jednak wywołanie funkcji zwrotnej nastąpiło tylko raz. Powodem tego jest ręczne pobranie kolejki zapisu metodą MutationObserver.takeRecords()
, która wyczyści kolejkę zapisu i tym samym uniemożliwi wywołanie funkcji zwrotnej po raz drugi.
Automatyczne tworzenie i usuwanie przejściowych zarejestrowanych obserwatorów zmian#
Koncepcja przejściowych zarejestrowanych obserwatorów zmian jest przypadkiem szczególnym i na pierwszy rzut oka może wydawać się niezrozumiała. Ma ona zastosowanie jedynie w sytuacji, kiedy usuwamy węzeł za pośrednictwem polecenia korzystającego z algorytmu usuwania, a na jednym z jego przodków istnieje zarejestrowany obserwator zmian, który ma śledzić zmiany także w potomkach usuwanego węzła, za co odpowiada właściwość subtree
w opcjach tego obserwatora zmian. Usunięty węzeł traci powiązanie ze swoimi wcześniejszymi przodkami, w zasadzie to staje się korzeniem dla swojej zawartości i od tej pory stanowi samodzielne drzewo węzłów, dlatego dalsza modyfikacja tego węzła nie powinna, przynajmniej w teorii, być zgłaszana przez wcześniejszych obserwatorów zmian.
W tym jednym szczególnym przypadku autorzy specyfikacji DOM przyjęli inne rozwiązanie. Po usunięciu węzła, w którym z wyżej wymienionych powodów miały być śledzone ewentualne modyfikacje tego węzła i jego potomków, tworzy się przejściowych zarejestrowanych obserwatorów zmian, z tymi samymi parametrami, co w obserwatorach zmian zarejestrowanych na jego wcześniejszych przodkach.
Istotne w tym wszystkim jest to, że przejściowi zarejestrowani obserwatorzy zmian nie są trwali, i po pierwszym zgłoszeniu obserwatorów zmian zostają automatycznie usunięci. W praktyce wygląda to tak, że wszelkie zmiany w usuniętym węźle zostaną zapisane i zgłoszone, ale tylko jeśli wykonamy je wsadowo (jedna po drugiej). Już po przetworzeniu zgłoszenia nastąpi automatyczne usunięcie wszystkich przejściowych zarejestrowanych obserwatorów zmian i dalsza modyfikacja usuniętego węzła nie będzie monitorowana.
Można dyskutować, czy powyższe rozwiązanie jest w ogóle potrzebne. Nic nie stoi na przeszkodzie, by w zależności o potrzeby po raz kolejny zarejestrować odpowiedniego obserwatora zmian na usuniętym węźle, z ustawioną właściwością subtree
w jego opcjach, który będzie trwały i mniej podatny na wewnętrzne autokorekty.
Prosty przykład:
<script>
// Tworzymy nowego obserwatora 'newObserver'
var newObserver = new MutationObserver(function(records, observer){
// W czasie wywołania funkcji zwrotnej mamy dostęp do:
// records - lista ze wszystkimi obiektami MutationRecord
// observer - nasz newObserwer
var getInfo = "";
var recordsLen = records.length;
for (var i = 0; i < recordsLen; i++){
var record = records[i];
getInfo += "<br><br>" + "records[" + i + "]: " + record
+ "<br>" + "record.type: " + record.type
+ "<br>" + "record.target: " + record.target
+ "<br>" + "record.addedNodes.length: " + record.addedNodes.length
+ "<br>" + "record.removedNodes.length: " + record.removedNodes.length
+ "<br>" + "record.attributeName: " + record.attributeName
+ "<br>" + "record.attributeNamespace: " + record.attributeNamespace
+ "<br>" + "record.oldValue: " + record.oldValue;
}
document.documentElement.innerHTML += "Liczba zmian records.length: " + recordsLen
+ "<br><br>" + "Odczytujemy poszczególne dane każdej zmiany:"
+ getInfo;
});
// Tworzymy obiekt konfiguracyjny
var config = {
childList: true,
attributes: true,
attributeOldValue: true,
attributeFilter: ["id", "name"],
subtree: true,
characterDataOldValue: true
};
var new_P = document.createElement("p");
new_P.innerHTML = "<span><b>Tekst1</b><b>Tekst2</b></span>";
var firstSpan = new_P.firstChild;
newObserver.observe(new_P, config); // rejestrujemy 'newObserver' na węźle 'new_P'
// Przeprowadzamy różne zmiany (wsadowo) na węźle 'new_P' i jego zawartości
firstSpan.remove();
firstSpan.firstChild.firstChild.textContent = "NowyTekst1";
firstSpan.lastChild.firstChild.textContent = "NowyTekst2";
firstSpan.firstChild.remove();
// Kolejne zmiany wykonujemy z opóźnieniem (już po automatycznym usunięciu przejściowego zarejestrowanego obserwatora zmian)
setTimeout(function(){
firstSpan.lastChild.firstChild.textContent = "NowyTekst3";
firstSpan.firstChild.remove();
}, 0);
</script>
Rozwiązanie przestarzałe (zdarzenia zmian)#
Wczesny DOM nie udostępniał żadnego natywnego mechanizmu pozwalającego rejestrować zmiany w węzłach. Dopiero z wydaniem "Document Object Model Level 2 Events" wprowadzono odpowiednie rozwiązanie w postaci zdarzeń zmian # (mutation events). Całość opierała się na nasłuchach zdarzeń podpinanych pod konkretne węzły.
Oto lista dodatkowych zdarzeń przewidzianych dla różnego rodzaju zmian:
DOMAttrModified
DOMCharacterDataModified
DOMNodeInserted
DOMNodeInsertedIntoDocument
DOMNodeRemoved
DOMNodeRemovedFromDocument
DOMSubtreeModified
Prosty przykład:
<!DOCTYPE html>
<html>
<head>
<script>
function addElement(localName){
var newElement = document.createElement(localName);
newElement.textContent = "Treść nowego elementu " + newElement.tagName + ".";
newElement.addEventListener("DOMNodeInserted", function(e) {
var info = document.getElementById("info");
info.innerHTML = "Nazwa interfejsu: " + e
+ "<br>" + "Event.target: " + e.target
+ "<br>" + "Event.relatedNode: " + e.relatedNode;
}, false);
document.body.appendChild(newElement);
}
</script>
</head>
<body>
<p>Do nowego elementu podczepiany jest nasłuch zdarzenia DOMNodeInserted.</p>
<p>Kliknij konkretny przycisk by utworzyć i dodać nowy element do dokumentu.</p>
<input type="button" value="createElement('p')" onclick="addElement('p')">
<input type="button" value="createElement('DIV')" onclick="addElement('DIV')">
<p style="color: blue;">Szczegółowe informacje o zdarzeniu:</p>
<p id="info"></p>
<p style="color: blue;">Nowe elementy dodane do dokumentu:</p>
</body>
</html>
Pomimo upływu lat zdarzenia zmian były w różnym stopniu implementowane przez przeglądarki, zazwyczaj niezgodnie ze standardem. Większym problemem okazała się jednak kwestia wydajnościowa; dodanie do dokumentu nasłuchu dla zdarzenia zmiany spowalniało przyszłą modyfikację dokumentu w granicach od 1,5 do 7 razy. Co więcej, usunięcie takiego nasłuchu nie odwracało poczynionych szkód.
Zważywszy na powyższe wady dopiero w specyfikacji "Document Object Model Level 3 Events" wszystkie zdarzenia zmian uznane są za przestarzałe (deprecated). Opis mechanizmu pozostał w specyfikacji zdarzeń, ale głównie ze względu na odniesienia i kompletność względem starszych programów i dokumentów.