[Перевод] Как работает JS: отслеживание изменений в DOM с помощью MutationObserver

Сегодня, в переводе десятого материала из серии, посвящённой особенностям работы механизмов JavaScript, мы расскажем о том, как отслеживать изменения в DOM с помощью API MutationObserver.

Клиентские части веб-приложений становятся всё сложнее, требуют всё больше системных ресурсов. Происходит это по разным причинам, в частности из-за того, что таким приложениям нужны продвинутые интерфейсы, благодаря которым раскрываются их возможности, и из-за того, что им приходится выполнять сложные вычисления на стороне клиента.

2kfagafcjkf1tie9uf8e1xeyits.jpeg

Всё это ведёт к усложнению задачи контроля состояния интерфейсов приложений в процессе их жизненного цикла. Эта задача становится ещё масштабнее в том случае, если речь идёт о разработке чего-то вроде фреймворка или даже обычной библиотеки, когда, например, нужно реагировать на то, что происходит со страницей и выполнять какие-то действия, зависящие от DOM.

Обзор


MutationObserver — это Web API, предоставляемое современными браузерами и предназначенное для обнаружения изменений в DOM. С помощью этого API можно наблюдать за добавлением или удалением узлов DOM, за изменением атрибутов элементов, или, например, за изменением текстов текстовых узлов. Зачем это нужно?

Есть немало ситуаций, в которых API MutationObserver может оказаться очень кстати. Например:

  • Вам нужно уведомить пользователя веб-приложения о том, что на странице, с которой он работает, произошли какие-то изменения.
  • Вы работаете над новым интересным JS-фреймворком, который динамически загружает JavaScript-модули, основываясь на изменениях DOM.
  • Возможно, вы работаете над WYSIWYG-редактором и пытаетесь реализовать функционал отмены и повтора действий. Воспользовавшись API MutationObserver, вы будете, в любой момент, знать о том, какие изменения произошли на странице, а это означает, что вы легко сможете их отменять.


f92b8fd52fd82f4cf8336a4aab5e7072.png


Текстовый редактор, работающий в браузере

Выше приведены лишь несколько ситуаций, в которых возможности MutationObserver могут оказаться полезными. На самом деле их гораздо больше.

Как пользоваться MutationObserver


Использовать MutationObserver в веб-приложениях довольно просто. Нужно создать экземпляр MutationObserver, передав соответствующему конструктору функцию, которая будет вызываться каждый раз, когда в DOM будут происходить изменения. Первый аргумент функции — это коллекция всех произошедших мутаций в виде единого пакета. Для каждой мутации предоставляется информация о её типе и об изменениях, которые она представляет.

var mutationObserver = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    console.log(mutation);
  });
});


У созданного объекта есть три метода:

  • Метод observe запускает процесс отслеживания изменений DOM. Он принимает два аргумента — узел DOM, за которым нужно наблюдать, и объект с параметрами.
  • Метод disconnect останавливает наблюдение за изменениями.
  • Метод takeRecords возвращает текущую очередь экземпляра MutationObserver, после чего очищает её.


Вот как включить наблюдение за изменениями:

// Запускаем наблюдение за изменениями в корневом HTML-элементе страницы
mutationObserver.observe(document.documentElement, {
  attributes: true,
  characterData: true,
  childList: true,
  subtree: true,
  attributeOldValue: true,
  characterDataOldValue: true
});


Теперь предположим, что в DOM имеется простейший элемент div:

Simple div


Используя jQuery, можно удалить атрибут class из этого элемента:

$("#sample-div").removeAttr("class");


Благодаря тому, что мы начали наблюдение за изменениями, предварительно вызвав mutationObserver.observe(...), и тому, что функция, реагирующая на поступление нового пакета изменений, выводит полученные данные в консоль, мы увидим в консоли содержимое соответствующего объекта MutationRecord:

6721bbe081f0fdebf5cbff7054607773.png


Объект MutationRecord

Тут можно видеть мутации, причиной которых стало удаление атрибута class.

И, наконец, для того, чтобы прекратить наблюдение за DOM после того, как работа завершена, можно сделать следующее:

// Прекратим наблюдение за изменениями
mutationObserver.disconnect();


Поддержка MutationObserver в различных браузерах


API MutationObserver пользуется широкой поддержкой в браузерах:

5c3cfee08678272258ad514bb295c27f.png


Поддержка MutationObserver

Альтернативы MutationObserver


Стоит отметить, что механизм наблюдениями за изменениями DOM, который предлагает MutationObserver, не всегда был доступен разработчикам. Чем они пользовались до появления MutationObserver? Вот несколько вариантов:

  • Опрос (polling).
  • Механизм MutationEvents.
  • CSS-анимация.


▍Опрос


Самый простой и незамысловатый способ отслеживания изменений DOM — опрос. Используя метод setInterval можно запланировать периодическое выполнение функции, которая проверяет DOM на предмет изменений. Естественно, использование этого метода значительно снижает производительность веб-приложений.

▍MutationEvents


API MutationEvents было представлено в 2000 году. Несмотря на то, что это API позволяет решать возлагаемые на него задачи, события мутации вызываются после каждого изменения DOM, что, опять же, приводит к проблемам с производительностью. Теперь API MutationEvents признано устаревшим и вскоре современные браузеры перестанут его поддерживать.

8bf7a5534a8414019536a79c921c813f.png


Поддержка MutationEvents

▍CSS-анимация


На самом деле, альтернатива MutationObserver в виде CSS-анимаций может показаться несколько странной. Причём тут анимация? В целом, идея тут заключается в создании анимации, которая будет вызвана после того, как элемент будет добавлен в DOM. В момент запуска анимации будет вызвано событие animationstart. Если назначить обработчик для этого события, можно узнать точное время добавления нового элемента в DOM. Время выполнения анимации при этом должно быть настолько маленьким, чтобы она была практически незаметна для пользователя.

Для того чтобы воспользоваться этим методом, сначала нужен родительский элемент, за добавлением в который новых узлов мы хотим наблюдать:


Для организации наблюдения за добавлением в него узлов нужно настроить последовательность ключевых кадров CSS-анимации, которые запустятся при добавлении узла:

@keyframes nodeInserted { 
 from { opacity: 0.99; }
 to { opacity: 1; } 
}


После создания ключевых кадров анимация должна быть применена к элементам, за которыми нужно наблюдать. Обратите внимание на длительность анимации. Она очень мала, благодаря чему анимация оказывается практически незаметной.

#container-element * {
 animation-duration: 0.001s;
 animation-name: nodeInserted;
}


Тут мы добавляем анимацию ко всем узлам-потомкам элемента container-element. Когда анимация заканчивается, вызывается соответствующее событие.

Теперь нужна JS-функция, которая будет играть роль обработчика событий. Внутри функции, в первую очередь, необходимо выполнить проверку event.animationName для того, чтобы убедиться, что это — именно та анимация, которая нас интересует.

var insertionListener = function(event) {
  // Убедимся в том, что это именно та анимация, которая нас интересует
  if (event.animationName === "nodeInserted") {
    console.log("Node has been inserted: " + event.target);
  }
}


Теперь добавим обработчик события к родительскому элементу. В разных браузерах это делается по-разному:

document.addEventListener("animationstart", insertionListener, false); // standard + firefox
document.addEventListener("MSAnimationStart", insertionListener, false); // IE
document.addEventListener("webkitAnimationStart", insertionListener, false); // Chrome + Safari


Вот как обстоит дело с поддержкой CSS-анимации в различных браузерах.

c766143040e14991b8d9782108e335b6.png


Поддержка CSS-анимации в различных браузерах

Итоги


Мы рассмотрели API MutationObserver и альтернативные способы наблюдения за изменениями DOM. Надо отметить, что MutationObserver имеет множество преимуществ перед этими альтернативами. В целом, можно говорить о том, что это API способно сообщать о любых изменениях, которые могут возникать в DOM, о том, что оно хорошо оптимизировано, давая информацию об изменениях, собранную в пакеты. Кроме того, API MutationObserver пользуется поддержкой всех основных современных браузеров, существуют и полифиллы для него, основанные на MutationEvents.

Автор материала отмечает, что MutationObserver занимает центральное место в библиотеке SessionStack, которая направлена на организацию сбора данных о том, что происходит с веб-страницами.

Предыдущие части цикла статей:

Часть 1: Как работает JS: обзор движка, механизмов времени выполнения, стека вызовов
Часть 2: Как работает JS: о внутреннем устройстве V8 и оптимизации кода
Часть 3: Как работает JS: управление памятью, четыре вида утечек памяти и борьба с ними
Часть 4: Как работает JS: цикл событий, асинхронность и пять способов улучшения кода с помощью async / await
Часть 5: Как работает JS: WebSocket и HTTP/2+SSE. Что выбрать?
Часть 6: Как работает JS: особенности и сфера применения WebAssembly
Часть 7: Как работает JS: веб-воркеры и пять сценариев их использования
Часть 8: Как работает JS: сервис-воркеры
Часть 9: Как работает JS: веб push-уведомления

Уважаемые читатели! Пользуетесь ли вы MutationObserver в своих проектах?

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru