Введение в 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. Это тоже может быть интересно:

4

Комментарии

  1. Максим

    Классная штука. Но видимо браузеры пока немного по разному реализовали спецификацию.
    Попробовал пример из статьи в хроме и фаерфоксе. В хроме выводится сразу новый текст, а в фаерфоксе — предыдущий.

    1. SelenIT

      SelenIT

      У меня и в Хроме (67), и в Фоксе (61, оба на винде 10) выводится предыдущий текст. Можно уточнить, как получилось вывести новый?

  2. Максим

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

    1. SelenIT

      SelenIT

      Спасибо, теперь вижу. Похоже на баг Хрома, причем скорее связанный с особенностями реализации contentEditable, а не самого MutationObserver: при вводе пробела он на самом деле вставляет в DOM неразрывный пробел (&nbsp;), а при вводе следующего символа заменяет его на обычный. Т.е. одно нажатие клавиши вызывает не одну мутацию, а две. И последней мутацией элемента, судя по всему, считается эта замена пробела, хотя визуально она ничего не меняет.

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Ваш E-mail не будет опубликован

Получать новые комментарии по электронной почте. Вы можете подписаться без комментирования.