MutationObserver

Obserwuje zmiany w drzewie DOM: dodane i usunięte elementy, zmiany atrybutów i tekstu, i zgłasza je w jednej funkcji.

Zwraca
Nowy obiekt MutationObserver.
Na tej stronie

Przykład

#
JavaScript
const chat = document.querySelector('#chat')
const unread = document.querySelector('#unread')

const observer = new MutationObserver((mutations) => {
  console.log(`Zmian w paczce: ${mutations.length}`)
  for (const mutation of mutations) {
    mutation.addedNodes.forEach((node) => console.log('Nowa wiadomość:', node.textContent))
  }
  unread.textContent = chat.children.length - 1
})

observer.observe(chat, { childList: true })

function addMessage(text) {
  const item = document.createElement('li')
  item.textContent = text
  chat.append(item)
}

addMessage('Kuba: Hej!')
addMessage('Ola: Kto robi quiz?')
console.log('Kod synchroniczny zakończony')
Konsola
Kod synchroniczny zakończony
Zmian w paczce: 2
Nowa wiadomość: Kuba: Hej!
Nowa wiadomość: Ola: Kto robi quiz?

Obie wiadomości dodano jedna po drugiej, więc obserwator dostał je w jednym wywołaniu, dopiero po zakończeniu kodu synchronicznego.

Definicja i zastosowanie

#

MutationObserver pozwala zareagować na zmiany w strukturze strony, niezależnie od tego, który fragment kodu je wprowadził. Przydaje się, gdy treść zmienia inny skrypt, biblioteka albo wtyczka i nie masz dostępu do miejsca, w którym to się dzieje, np. przy liczeniu nowych wiadomości w czacie albo pilnowaniu atrybutów widżetu.

Metoda observe(element, opcje) wymaga wskazania, co obserwować: childList (dodawanie i usuwanie dzieci), attributes (zmiany atrybutów) albo characterData (zmiany tekstu w węzłach tekstowych). Opcja subtree rozszerza obserwację na wszystkich potomków, attributeFilter zawęża ją do wybranych atrybutów, a attributeOldValue zapamiętuje ich poprzednie wartości.

Funkcja zwrotna dostaje tablicę obiektów MutationRecord z polami takimi jak type, target, addedNodes, removedNodes i attributeName. Zmiany są zbierane i przekazywane razem jako mikrozadanie, czyli po zakończeniu bieżącego kodu, ale przed timerami. Kilka zmian wykonanych jedna po drugiej trafi więc do jednego wywołania.

Metoda disconnect() kończy obserwację, a takeRecords() zwraca zebrane, jeszcze nieprzekazane zmiany. Unikaj obserwowania całego dokumentu z subtree: true, jeśli nie jest to konieczne, bo przy dużych zmianach funkcja zwrotna będzie wywoływana bardzo często.

Składnia

#
Składnia
new MutationObserver(callback)

observer.observe(target, options)
observer.disconnect()
observer.takeRecords()

Parametry

#
  • callback

    function (mutations, observer)

    Funkcja wywoływana po zmianach. Dostaje tablicę obiektów MutationRecord.
  • target

    węzeł

    Element (lub inny węzeł), od którego zaczyna się obserwacja.
  • options

    obiekt

    Co najmniej jedna z opcji childList, attributes lub characterData ustawiona na true, a do tego opcjonalnie subtree, attributeFilter, attributeOldValue i characterDataOldValue.

Więcej przykładów

#
Zmiany atrybutu z poprzednią wartością
JavaScript
const lesson = document.querySelector('#lesson')

const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    console.log(`${mutation.attributeName}, poprzednia wartość: ${mutation.oldValue}`)
  }
  console.log('Aktualny status:', lesson.dataset.status)
})

observer.observe(lesson, { attributeFilter: ['data-status'], attributeOldValue: true })

lesson.dataset.status = 'in-progress'
lesson.dataset.status = 'done'
lesson.title = 'Ukończona'
Konsola
data-status, poprzednia wartość: todo
data-status, poprzednia wartość: in-progress
Aktualny status: done

Zmiana title nie trafia do obserwatora, bo attributeFilter ogranicza obserwację do data-status. Każda zmiana ma osobny wpis z wartością sprzed zmiany.

Całe poddrzewo i disconnect()
JavaScript
const editor = document.querySelector('#editor')
const note = document.querySelector('#note')

const observer = new MutationObserver((mutations) => {
  console.log(mutations.map((mutation) => mutation.type))
})

observer.observe(editor, { childList: true, characterData: true, subtree: true })

note.firstChild.data = 'Notatka o pętlach'
editor.append(document.createElement('hr'))

setTimeout(() => {
  observer.disconnect()
  note.firstChild.data = 'Tej zmiany nikt nie zobaczy'
  console.log('Obserwacja zakończona')
}, 100)
Konsola
['characterData', 'childList']
Obserwacja zakończona

Zmiana tekstu w węźle tekstowym to characterData, a dodanie <hr> to childList. Tekst leży głębiej niż #editor, dlatego potrzebna jest opcja subtree. Po disconnect() kolejne zmiany nie są już zgłaszane.

Obsługa przeglądarek

#

Szeroko dostępne

Działa we wszystkich nowoczesnych przeglądarkach, także na telefonach. Możesz używać bez obaw.

  • Chrome
  • Edge
  • Firefox
  • Safari

Dobre praktyki

#
  • Zawsze podawaj w opcjach, co obserwować. Samo observe(element) bez childList, attributes ani characterData rzuci błąd TypeError.
  • Nie zmieniaj obserwowanego fragmentu w funkcji zwrotnej bez warunku. Każda taka zmiana wywoła obserwatora ponownie i łatwo o nieskończoną pętlę.
  • Jeśli sam zmieniasz DOM w swoim kodzie, prościej wywołać odpowiednią funkcję zaraz po zmianie. Obserwator przydaje się, gdy zmiany wprowadza kod, na który nie masz wpływu.

Powiązane hasła

#

Widzisz błąd albo brakuje przykładu? Napisz do nas.