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

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



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

Обзор


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

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

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


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

Выше приведены лишь несколько ситуаций, в которых возможности 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:

<div id="sample-div" class="test"> Simple div </div>

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

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

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


Объект MutationRecord

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

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

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

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


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


Поддержка MutationObserver

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


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

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

?Опрос


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

?MutationEvents


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


Поддержка MutationEvents

?CSS-анимация


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

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

<div id="container-element"></div>

Для организации наблюдения за добавлением в него узлов нужно настроить последовательность ключевых кадров 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-анимации в различных браузерах.


Поддержка 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 в своих проектах?

Комментарии (6)


  1. VolCh
    16.03.2018 12:00

    Использовал для двух кейсов:


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


  1. rework
    16.03.2018 17:03

    Честно говоря, использование MutationObserver кажется на добавление костылей в код. Ведь нас столько лет убеждали, что view не должен меняться сам по себе, мы должны контролировать его изменения, который могу произойти только при разнообразных асинхронных событиях (Таймеры, пользовательские события ввода, ответ от AJAX запросов, события от сервера через сокеты и т.д.). Если у вас возникает желание подписаться напрямую на изменения в DOM, то скорее всего что-то не так с архитектурой вашего приложения. В голову приходит только один кейс, когда какая-либо сторонняя библиотека вносит изменения в DOM и мы не можем напрямую подписаться на события которые к этому приводят.


    1. napa3um
      18.03.2018 20:14

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


  1. movl
    16.03.2018 17:15

    Когда в StreamKeys добавлял поддержку нового плеера VK, не нашел другого адекватного способа отслеживать изменения, будучи сторонним наблюдателем. Решил использовать это API, так как инициализация плеера происходила в два действия, сначала кликаешь на плеер в шапке, потом на кнопку плей (не знаю как сейчас), а между действиями ожидаешь неопределенное количество времени, плюс это позволило улучшить поддержку старого плеера, что еще было актуально.


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


  1. avlapeto
    16.03.2018 19:05

    Использовали в проекте чтобы отловить асинхронные изменения сделанные сторонним скриптом (аппенд отдельного элемента) и запустить реакцию на эти изменения. Звучит как костыль, но позволило элегантно решить проблему.


  1. houligan
    17.03.2018 01:29

    Незаменимый инструмент для интеграции веб-аналитики. Буквально на всё, что угодно, можно поставить «сенсоры». Особенно актуально, когда нет прямого доступа к исходному коду приложения, и единственная возможная среда интеграции — тег менеджеры на javascript (Google Tag Manager, Tealium и подобные).