Введение в API MutationObserver из JavaScript
Перевод статьи An introduction to the JavaScript MutationObserver API с сайта benfrain.com, автор — Бен Фрейн.
Недавно мне довелось немного поиграть с JavaScript-овым API MutationObserver и я был приятно шокирован. Я уже наметил те места, где я, наверное, мог бы сделать код чище с помощью него. Если не слышали о нём раньше, вот небольшой пример
MDN описывает интерфейс MutationObserver так:
С помощью интерфейса MutationObserver можно наблюдать за изменениями, происходящими в DOM-дереве.
Вы не сильно ошибётесь, если представите их в качестве обработчиков событий для изменений DOM-элементов.
Поддержка также хорошая. IE11 и все вечнозелёные браузеры на десктопе. На мобильных это Android 4.4 и выше и iOS6.
Базовый пример
Позвольте показать быстрый пример. Предположим, у нас есть редактируемый (с атрибутом contenteditable) кусок текста, и нужно что-то сделать, когда пользователь правит его. Например, мы хотим знать, какой текст был до того, как пользователь нажал клавишу.
See the Pen MutationObserver by Максим (@psywalker) on CodePen.
Итак, при вот такой разметке:
<div class="container"> <div class="value" contenteditable="true">type something here</div> </div> <div class="previous"></div>
Мы можем использовать этот JavaScript-код:
const container = document.querySelector(".container"); const previous = document.querySelector(".previous"); const mutationConfig = { attributes: true, childList: true, subtree: true, characterData: true, characterDataOldValue: true }; var onMutate = function(mutationsList) { mutationsList.forEach(mutation => { previous.textContent = mutation.oldValue; }); }; var observer = new MutationObserver(onMutate); observer.observe(container, mutationConfig);
При каждом нажатии клавиши видно, какой была строка текста до этого. И не нужно хранить существующее значение в переменной или беспокоиться об обработке события keyup
, оно просто есть в MutationRecord
мутации, который предоставляется с каждым объектом Mutation
. Если вы выведете мутацию в лог внутри forEach
выше, то увидите этот MutationRecord
в консоли. Я слушал characterData
, но если бы вы вставили/удалили DOM-ноды, то могли бы подробно рассмотреть и это.
Анатомия написания MutationObserver
Так, мы узнали, на что вообще способен MutationObserver
, что же на самом деле этот код делает?
Во-первых, мы просто захватываем элемент-контейнер. Заметили, что мы захватываем родительский элемент, где происходят изменения? Это связано с тем, что можно установить какую угодно область действия MutationObserver
, если надо — шире, если надо — теснее. Мы также захватили элемент `previous`, в котором мы пишем предыдущий текст.
const container = document.querySelector(".container"); const previous = document.querySelector(".previous");
Далее идет конфигурация, которую я хочу передать в этот раз в MutationObserver
const mutationConfig = { attributes: true, childList: true, subtree: true, characterData: true, characterDataOldValue: true };
Необязательно делать это отдельным `const`
, вместо этого я могу запросто передать его параметром при вызове метода вот так:
observer.observe(container, { attributes: true, childList: true, subtree: true, characterData: true, characterDataOldValue: true });
Затем идет функция, которую я хочу запустить при обнаружении каких-либо мутаций. Я придумал для нее оригинальное название onMutate
:
var onMutate = function(mutationsList) { mutationsList.forEach(mutation => { previous.textContent = mutation.oldValue; }); };
Здесь передаётся список мутаций и для каждой из них я выписываю oldValue
из мутации в DOM. Боюсь показаться капитаном Очевидность, но тут можно всё что угодно, учитывая, какую бездну возможностей предоставляет MutationObserver
.
Вы фактически создаёте MutationObsever
с ключевым словом new
и именем колбека, который нужно запустить, когда произойдет мутация, за которой мы наблюдаем.
var observer = new MutationObserver(onMutate);
Теперь, когда у нас есть объект-наблюдатель, мы можем наблюдать за ним вот так
observer.observe(container, mutationConfig);
Мы передаем элемент, за которым хотим наблюдать, и конфигурацию, с помощью которой должны обрабатываться любые мутации.
Опции для MutationObserver
Учитывайте, что в настоящий момент на странице MDN не хватает информации о опциях, доступных в конфиге MutationObserver. Это подробно описано в спецификации на https://dom.spec.whatwg.org/#mutationobserver.
Для полноты картины вот они:
- childList
- attributes
- characterData
- subtree
- attributeOldValue
- characterDataOldValue
- attributeFilter
В нашем маленьком демо нас интересуют опции characterData
и characterDataOldValue
. Без них мы бы ничего не увидели. Эти опции — отличный способ игнорировать всё лишнее в зависимости от ваших требований.
У MutationObserver также есть метод takeRecords()
, который возвращает всё, что находится в очереди записей, и метод disconnect
, останавливающий наблюдатель.
Заключение
По-моему, API MutationObserver даёт очень чистый способ работы с изменением DOM помимо более привычных обработчиков элементов input/form. Поддержка отличная, и API восхитительно прост и, по крайней мере для меня, очень логичен.
Если вы, как и я, не пробовали их раньше, очень советую.
Обновление
Роберт Смит отметил (в твиттере), что у Кента Доддса есть библиотека DOM Testing, которая эффективно использует MutationObserver. Вот что пишет сам Кент в Твиттере:
Функция waitForElement из библиотеки dom-testing-library использует MutationObserver, чтобы как можно скорее узнать, когда вызывать колбек и проверить, доступен ли элемент, который вы ожидаете! Очень интересный API!
Безусловно, примеры из реальной жизни встречаются, хотя кажется, что этот API всё-таки несколько недооценен.
P.S. Это тоже может быть интересно:
Классная штука. Но видимо браузеры пока немного по разному реализовали спецификацию.
Попробовал пример из статьи в хроме и фаерфоксе. В хроме выводится сразу новый текст, а в фаерфоксе — предыдущий.
У меня и в Хроме (67), и в Фоксе (61, оба на винде 10) выводится предыдущий текст. Можно уточнить, как получилось вывести новый?
Немного поспешил. Различия все-таки есть. В хроме если ввести пробел, и затем продолжить вводить другие символы, то первый символ, введенный после пробела, отображается сразу.
Спасибо, теперь вижу. Похоже на баг Хрома, причем скорее связанный с особенностями реализации
contentEditable
, а не самогоMutationObserver
: при вводе пробела он на самом деле вставляет в DOM неразрывный пробел (
), а при вводе следующего символа заменяет его на обычный. Т.е. одно нажатие клавиши вызывает не одну мутацию, а две. И последней мутацией элемента, судя по всему, считается эта замена пробела, хотя визуально она ничего не меняет.