JavaScript MutationObserver API
Изучите JavaScript MutationObserver API: наблюдение за изменениями DOM, методы observe/disconnect/takeRecords, MutationRecord, паттерны и примеры.
MutationObserver API в JavaScript позволяет следить за частью DOM и запускать callback-функцию всякий раз, когда что-то внутри изменяется — добавляется или удаляется узел, редактируется атрибут или обновляется текстовое содержимое. В отличие от устаревших mutation events, которые он заменил, MutationObserver группирует изменения и доставляет их асинхронно, не блокируя страницу и не генерируя отдельное событие для каждой мелкой правки.
В этом руководстве рассматривается, за чем можно наблюдать, три метода, которые предоставляет каждый observer, структура объекта MutationRecord, реальные сценарии использования, распространённые ловушки и интерактивный пример, который можно запустить.
Когда использовать MutationObserver?
MutationObserver применяют тогда, когда контент, которым вы не управляете, изменяется и вам нужно на это реагировать:
- Сторонний / встраиваемый контент — виджет, реклама или блок, отрендеренный CMS, появляется в DOM и его нужно стилизовать или дополнить.
- Ожидание появления элемента — ожидание узла, который фреймворк отрендерит позже, вместо опроса через
setInterval. - Синхронизация UI с изменениями атрибутов — реакция на изменение
class,style,disabledили атрибутаdata-*на элементе, к которому нельзя добавить обработчик. - Авторесайз или перемерка — пересчёт разметки при добавлении дочерних элементов в контейнер.
- Синхронизация модели с текстом в
contenteditable.
Если вам нужно только знать, когда элемент входит или покидает область видимости, используйте IntersectionObserver — он создан специально для этой задачи и обходится дешевле.
Три метода
Каждый экземпляр observer предоставляет ровно три метода:
| Метод | Что делает |
|---|---|
observe(target, options) | Начинает наблюдение за target с помощью объекта options. Вызовите снова с другим target, чтобы наблюдать за несколькими узлами одним observer. |
disconnect() | Прекращает наблюдение за всеми целями. Callback больше не будет вызываться. Всегда вызывайте этот метод, когда работа завершена, чтобы избежать утечек памяти. |
takeRecords() | Синхронно возвращает (и очищает) все ожидающие MutationRecords, которые ещё не были доставлены в callback. Полезно вызвать непосредственно перед disconnect(), чтобы не потерять последнюю порцию данных. |
Объект options, передаваемый в observe(), должен содержать хотя бы одно из полей childList, attributes или characterData, иначе вызов бросит TypeError.
| Опция | Значение |
|---|---|
childList | Следить за добавлением/удалением прямых дочерних узлов. |
attributes | Следить за изменениями атрибутов. |
characterData | Следить за изменениями данных текстового узла. |
subtree | Распространить наблюдение на всех потомков, а не только на target. |
attributeOldValue | Записывать предыдущее значение атрибута (подразумевает attributes). |
characterDataOldValue | Записывать предыдущее текстовое значение (подразумевает characterData). |
attributeFilter | Array имён атрибутов для наблюдения — остальные игнорируются. |
Пример Mutation Observer
Вот базовый пример, демонстрирующий работу Mutation Observer путём визуального отображения изменений в DOM.
<!DOCTYPE html>
<html>
<head>
<title>Exploring DOM Changes: Live Examples with Mutation Observers</title>
</head>
<body>
<div id="target" style="background-color: lightgray; padding: 10px;">
Watch this space for changes!
</div>
<button style="margin-top: 10px;" onclick="addNewElement(); changeAttribute();">Add New Element and Change Color</button>
<div id="log" style="margin-top: 20px;"></div>
<script>
// Get the element to observe
const targetNode = document.getElementById('target');
// Define configurations for the observer
const config = { attributes: true, childList: true, subtree: true, attributeOldValue: true };
// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
for (const mutation of mutationsList) {
const message = document.createElement('p');
if (mutation.type === 'childList') {
message.textContent = 'A child node has been added or removed.';
message.style.color = 'green';
} else if (mutation.type === 'attributes') {
message.textContent = 'The ' + mutation.attributeName + ' attribute was modified.';
message.style.color = 'blue';
}
document.getElementById('log').appendChild(message);
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// Function to add new elements
function addNewElement() {
const newElement = document.createElement('div');
newElement.textContent = 'New element added!';
targetNode.appendChild(newElement);
}
// Function to change attributes
function changeAttribute() {
const currentColor = targetNode.style.backgroundColor;
targetNode.style.backgroundColor = currentColor === 'lightgray' ? 'lightblue' : 'lightgray';
}
</script>
</body>
</html>Этот пример демонстрирует использование Mutation Observer для обнаружения изменений в объектной модели документа (DOM) веб-страницы и реагирования на них. Вот что делает каждая часть JavaScript и что вы увидите при взаимодействии с примером:
- Настройка Mutation Observer:
- Целевой узел: DOM-элемент, за которым вы хотите наблюдать. В данном случае это
divс IDtarget. - Конфигурация: определяет, какие типы изменений нужно отслеживать:
attributes: observer будет следить за изменениями атрибутов (например, style или class).childList: будет проверять добавление или удаление дочерних элементов (например, новых div).subtree: обеспечивает проверку не только целевого элемента, но и его потомков. Обратите внимание, чтоsubtreeдействует только при включённомchildListилиattributes.attributeOldValue: записывает предыдущее значение изменённого атрибута (полезно для отслеживания изменений).
- Целевой узел: DOM-элемент, за которым вы хотите наблюдать. В данном случае это
- Определение callback-функции:
- Эта функция выполняется каждый раз, когда observer обнаруживает изменение согласно заданной конфигурации.
- Она перебирает все обнаруженные мутации и создаёт сообщение журнала для каждой из них:
- Если дочерний элемент добавлен или удалён, выводится "A child node has been added or removed." зелёным текстом.
- Если атрибут изменился (например, цвет фона), выводится "The
mutation.attributeNameattribute was modified." синим текстом.
- Экземпляр Observer:
- Mutation Observer создаётся и связывается с callback-функцией.
- Начало наблюдения:
- Observer начинает отслеживать изменения в
divс IDtargetсогласно указанной конфигурации.
- Observer начинает отслеживать изменения в
- Интерактивные функции:
- Добавление нового элемента: при нажатии кнопки добавляется новый div с текстом "New element added!" внутри
divс IDtarget. - Изменение атрибута: то же нажатие кнопки переключает цвет фона
divс IDtargetмежду 'lightgray' и 'lightblue'. Примечание: хотя встроенныйonclickподходит для данного примера, в продакшене рекомендуется использоватьaddEventListenerдля более чистого разделения кода.
- Добавление нового элемента: при нажатии кнопки добавляется новый div с текстом "New element added!" внутри
Ожидаемые результаты:
- Добавление нового элемента:
- При каждом нажатии кнопки добавляется новый div. Это запускает проверку childList observer, и вы увидите зелёное сообщение "A child node has been added or removed."
- Изменение атрибута:
- То же нажатие кнопки изменит цвет фона
divс IDtarget. Это запускает проверку атрибутов observer. Вы увидите синее сообщение с указанием изменённого атрибута ("The style attribute was modified.").
- То же нажатие кнопки изменит цвет фона
Данный пример наглядно показывает, как Mutation Observers могут использоваться для мониторинга и записи изменений в DOM, обеспечивая обратную связь в реальном времени о происходящем на странице.
Чтение MutationRecord
Callback получает array объектов MutationRecord — по одному на каждое обнаруженное изменение. Наиболее полезные свойства:
type—"childList","attributes"или"characterData".target— узел, которого коснулась мутация.addedNodes/removedNodes—NodeLists вставленных/удалённых узлов (дляchildList).attributeName— изменённый атрибут (дляattributes).oldValue— предыдущее значение, но только если было включеноattributeOldValueилиcharacterDataOldValue.
const observer = new MutationObserver((records) => {
for (const record of records) {
if (record.type === "attributes") {
console.log(`${record.attributeName} changed from "${record.oldValue}"`);
} else if (record.type === "childList") {
console.log(`+${record.addedNodes.length} / -${record.removedNodes.length} nodes`);
}
}
});Практический паттерн: ожидание появления элемента
Один из распространённых реальных сценариев — выполнение Promise в момент появления узла в DOM, что намного лучше опроса. Observer отключается сам, как только находит элемент:
function waitForElement(selector) {
return new Promise((resolve) => {
const existing = document.querySelector(selector);
if (existing) return resolve(existing);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect(); // stop watching once found
resolve(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
}
// Usage with async/await:
// const card = await waitForElement(".lazy-card");Это хорошо сочетается с async/await и Promises.
Ловушки и рекомендации
- Callback асинхронен и пакетный. Мутации ставятся в очередь как microtasks и доставляются после завершения текущего скрипта — см. microtasks и event loop. Вы не получите запись для каждого отдельного изменения — они приходят группой.
- Ваш callback выполняется после изменения. Вас уведомляют о том, что уже произошло — отменить или предотвратить мутацию, как это делает
preventDefault()для события, нельзя. oldValueтребует явного включения. Если вы читаетеrecord.oldValueбез включенияattributeOldValue/characterDataOldValue, значение будетnull.- Избегайте изменения наблюдаемого поддерева внутри callback, если это не запланировано — это может породить новые записи и создать петлю обратной связи.
- Всегда вызывайте
disconnect(). Активный observer удерживает свою цель в памяти, поэтому забытыйdisconnect()приводит к утечке памяти. Отключайтесь при размонтировании компонента или по завершении работы. - Вызывайте
takeRecords()перед отключением, если важна последняя порция данных —disconnect()удаляет недоставленные записи.
Заключение
Mutation Observers — важный инструмент в арсенале JavaScript, предлагающий динамические решения для эффективного управления изменениями DOM. Они позволяют разработчикам создавать отзывчивые, интерактивные веб-приложения, которые плавно реагируют на действия пользователя и программные изменения DOM. При всей своей мощи, Mutation Observers следует использовать взвешенно, чтобы сохранять оптимальную производительность и качество пользовательского опыта. Тщательно выбирая, какие мутации отслеживать, минимизируя накладные расходы в callback-функциях и вызывая observer.disconnect(), когда observer больше не нужен, разработчики могут применять Mutation Observers для расширения функциональности сайта без ущерба для эффективности. Понимание и применение этих принципов позволяет создавать продвинутые, удобные для пользователя веб-интерфейсы, выделяющиеся в современном цифровом пространстве.