Клиентские части веб-приложений становятся всё сложнее, требуют всё больше системных ресурсов. Происходит это по разным причинам, в частности из-за того, что таким приложениям нужны продвинутые интерфейсы, благодаря которым раскрываются их возможности, и из-за того, что им приходится выполнять сложные вычисления на стороне клиента.
Всё это ведёт к усложнению задачи контроля состояния интерфейсов приложений в процессе их жизненного цикла. Эта задача становится ещё масштабнее в том случае, если речь идёт о разработке чего-то вроде фреймворка или даже обычной библиотеки, когда, например, нужно реагировать на то, что происходит со страницей и выполнять какие-то действия, зависящие от 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)
rework
16.03.2018 17:03Честно говоря, использование MutationObserver кажется на добавление костылей в код. Ведь нас столько лет убеждали, что view не должен меняться сам по себе, мы должны контролировать его изменения, который могу произойти только при разнообразных асинхронных событиях (Таймеры, пользовательские события ввода, ответ от AJAX запросов, события от сервера через сокеты и т.д.). Если у вас возникает желание подписаться напрямую на изменения в DOM, то скорее всего что-то не так с архитектурой вашего приложения. В голову приходит только один кейс, когда какая-либо сторонняя библиотека вносит изменения в DOM и мы не можем напрямую подписаться на события которые к этому приводят.
napa3um
18.03.2018 20:14Да, тоже считаю, что этот инструмент не для использования внутри одного приложения, а, скорее, для написания браузерных плагинов и сервисов, работающих со сторонними сайтами, не предоставляющими API для взаимодействия. Парсинг, автоматизированное тестирование GUI и т.п.
movl
16.03.2018 17:15Когда в StreamKeys добавлял поддержку нового плеера VK, не нашел другого адекватного способа отслеживать изменения, будучи сторонним наблюдателем. Решил использовать это API, так как инициализация плеера происходила в два действия, сначала кликаешь на плеер в шапке, потом на кнопку плей (не знаю как сейчас), а между действиями ожидаешь неопределенное количество времени, плюс это позволило улучшить поддержку старого плеера, что еще было актуально.
Очень специфичные кейсы у этого API все же, но для каких-нибудь тестов или статистики, почему бы и нет.
avlapeto
16.03.2018 19:05Использовали в проекте чтобы отловить асинхронные изменения сделанные сторонним скриптом (аппенд отдельного элемента) и запустить реакцию на эти изменения. Звучит как костыль, но позволило элегантно решить проблему.
houligan
17.03.2018 01:29Незаменимый инструмент для интеграции веб-аналитики. Буквально на всё, что угодно, можно поставить «сенсоры». Особенно актуально, когда нет прямого доступа к исходному коду приложения, и единственная возможная среда интеграции — тег менеджеры на javascript (Google Tag Manager, Tealium и подобные).
VolCh
Использовал для двух кейсов: