Frontender Magazine

Познакомимся с Mutation Observers

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

Раньше мы делали это с помощью событий изменения. В спецификации DOM Level 2 интерфейс MutationEvent определял несколько событий: например, DOMNodeInserted и DOMAttrModified — они вызываются браузером, когда был добавлен, изменен или удален элемент. Однако в событиях, сообщающих об изменении DOM есть свои проблемы.

Проблемы с Mutation Events

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

  1. MutationEvent вызывается синхронно. События срабатывают тогда, когда они вызваны, и другие события в очереди могут из-за этого задержаться. Добавьте или удалите из документа достаточное количество элементов, и приложение начнет тормозить или зависнет.
  2. MutationEvent — события, и поэтому были реализованы как события. Я понимаю, что это звучит слишком очевидно, но подождите секунду. События проходят через DOM — они захватываются или всплывают. Оба этих процесса могут, в свою очередь, заставлять срабатывать другие обработчики событий, которые меняют DOM. И, соответственно, те заставят сработать еще большее количество MutationEvents, отчего поток для исполнения JavaScript будет забит — а в худшем случае браузер рухнет.

Звучит жутко, не правда ли?

Действительно, с событиями изменения DOM возникает столько проблем, что они были помечены как устаревшие в спецификации DOM Level 3. Но если события изменения устарели, нам нужно что-то, что могло бы их заменить. Вот здесь и появляются наблюдатели за изменениями (Mutation Observers).

Чем Mutation Observers отличаются от Mutation Events?

Наблюдатели за изменениям определяются стандартом DOM, и отличаются от событий изменения DOM следующим ключевым образом: они являются асинхронными. Они не срабатывают каждый раз, как случается событие. Вместо этого они:

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

Давайте посмотрим пример. В коде ниже мы прибавляем к фрагменту документа 2500 абзацев, а потом добавляем этот фрагмент в качестве дочернего к элементу article.

var docFrag  = document.createDocumentFragment(),
    thismany = 2500,
    i=0,
    a = document.querySelector('article'),
    p;

while ( i < thismany) {
    // Создает новый элемент p, если он существует.
    // Если таковой существует, то клонируем существующий элемент.
    p = (p === undefined) ? document.createElement('p') : p.cloneNode(false);
    docFrag.appendChild(p);
    i++;
}

a.appendChild( docFrag );

Листинг 1: Добавление 2500 абзацев к документу через фрагмент документа.

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

А их уже можно использовать?

Поддержка пока доступна не везде. Интерфейс MutationObserver поддерживается в Opera 15+, Firefox 14+ и Chrome 26+. Его также будет поддерживать Internet Explorer 11 и Safari 6.1. Safari 6.0 и Chrome 18—25 поддерживают MutationObserver, но с WebKit-префиксом (WebKitMutationObserver). Определить, поддерживается ли наблюдение за изменениями, можно с помощью следующего кода.

var canObserveMutation = 'MutationObserver' in window;

Листинг 2: Определяем, поддерживается ли браузером наблюдение за изменениями.

Так, как же мне использовать MutationObserver?

Хорошие новости! Это легко. Сперва создаете объект-наблюдатель с помощью конструктора MutationObserver, как в листинге 3. В конструкторе указываете единственный параметр — функцию-колбэк.

var observer, callback;
callback = function( recordqueue ){
    // сделать что-то с каждой записью в массиве recordqueue.
}
observer = new MutationObserver( callback );

Листинг 3: Создаем наблюдатель.

Колбэк-функция получит в качестве аргумента массив объектов MutationRecord. Каждый объект MutationRecord описывает изменение в дереве элементов. Мы подробнее обсудим MutationRecord позже.

Далее нужно будет определить, за каким элементом вы будете следить и какие типы изменений в DOM вам интересны. Для этого мы используем метод observe. Его первый параметр — элемент, а второй — словарь настроек (листинг 4). В примере ниже мы будем наблюдать за изменениями дочерних элементов или атрибутов элемента article.

var  options = {
    'childList': true,
    'attributes':true
},
article = document.querySelector( 'article' );

observer.observe( article, options );

Листинг 4: Решаем, какой элемент и тип изменения мы будем отслеживать

Параметр options может включать следующие свойства и значения:

Для того, чтобы следить за изменением, необходимо включить значения childList, attributes или characterData, и хотя бы одному из них должно быть присвоено значение true.

Для того, чтобы перестать следить за изменениями, используйте метод disconnect() (observer.disconnect()). После использования этого метода колбэк больше не будет вызываться. Метод takeRecord (observer.takeRecord()) очищает очередь записей. Для того, чтобы продолжить следить за изменениями, просто снова вызовите метод observe.

Я упомянул, что колбэк об изменении получает массив записей изменений в качестве аргумента. Давайте посмотрим на то, что такое запись изменений.

MutationRecord

Запись изменений — объект, который содержит информацию об одном изменении в дереве документа. Объекты записи изменений соответствуют интерфейсу MutationRecord и содержат следующие объекты.

Теперь, когда мы рассмотрели синтаксис наблюдателей и записей изменений, давайте посмотрим на примеры.

Наблюдение за добавлением и удалением дочерних элементов

Наблюдать за добавлением и удалением дочерних элементов довольно просто. Мы создаем новый объект и передаем ему колбэк. Следить мы будем за body и изменениями во всех его дочерних элементах. Пример в листинге 5.

var callback = function(allmutations){
    // allmutations — массив, и мы можем использовать соответствующие методы JavaScript.
    allmutations.map( functions(mr){
        var mt = 'Тип изменения: ' + mr.type;  // записываем тип изменения
        mt += 'Измененный элемент: ' + mr.target; // записываем измененный элемент.
        console.log( mt );
    });

},
mo = new MutationObserver(callback),
options = {
    // обязательный параметр: наблюдаем за добавлением и удалением дочерних элементов.
    'childList': true,
    // наблюдаем за добавлением и удалением дочерних элементов любого уровня вложенности.
    'subtree': true
}
mo.observe(document.body, options);

Листинг 5: Наблюдаем за добавлением и удалением дочерних элементов в документе.

Обратите внимание, что мы включили опцию subtree и установили ее значение равным true. Это значит, что наблюдатель будет получить информацию о том, когда к телу документа добавляются дочерние элементы (например: document.body.appendChild(el)), а также когда они прибавляются к дочернему элементу документа (document.getElementById('my_element').appendChild(el)). Если бы параметр subtree был установлен в false или не был бы указан, то наблюдатель следил бы только за элементами, добавляемыми непосредственно к телу документа.

Можно также наблюдать за изменениям в фрагментах документа. Просто передайте фрагмент как первый параметр методу observe.

Наблюдаем за изменениями атрибутов

Наблюдение за изменениями атрибутов работает во многом так же. Главная разница в том, что к словарю опций вам нужно добавить 'attributes': true. Если вы хотите получать предыдущее значение атрибута, то установите в true значение attributeOldValue(посмотреть демо).

var callback = function(allmutations){
    // allmutations — массив, и мы можем использовать соответствующие методы JavaScript.
    allmutations.map( functions(mr){
        // записываем предыдущее значение атрибута.
        var attr = 'Предыдущее значение атрибута: ' + mr.oldValue;
        console.log(attr);
    });
},
element = document.getElementById('my_el'),
mo = new MutationObserver(callback),
options = {
    'attributes': true,        // обязательно
    'attributeOldValue': true  // перехватываем предыдущее значение атрибута.
}

mo.observe(element, options);

Листинг 6: Наблюдаем за изменениями значений атрибутов.

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

Фильтруем атрибуты, за которыми мы наблюдаем

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

var options = {
    'attributes': true,
    'attributeOldValue': true,
    'attributeFilter': ['class'] // наблюдаем только за изменениями атрибута class
}

mo.observe(element, options);

Листинг 7: Фильтруем атрибуты, за которыми мы наблюдаем.

Установка этого свойства означает, что запись изменения будет сгенерирована только для изменений значения атрибута class (посмотреть демо).

Дополнительная информация

Чтобы узнать больше о наблюдателях, обратите внимание на следующие ресурсы:

Если вы заметили ошибку, вы всегда можете отредактировать статью, создать issue или просто написать об этом Антону Немцеву в skype ravencry.

Tiffany Brown
Автор:
Tiffany Brown
GitHub:
webinista
Twitter:
@webinista
Сaйт:
http://webinista.com/
Email:
tiffany@webinista.com
Vlad Andersen

Комментарии (2 комментария, если быть точным)

Автар пользователя
firuz1844

Поправьте опечатку в Листинге 6 в строке: allmutations.map( functions(mr){ не должно быть s allmutations.map( function(mr){

Автар пользователя
firuz1844

а статья полезная! спасибо )