Знакомство с MutationObserver API
Опубликовано: 2022-03-10В сложных веб-приложениях изменения DOM могут происходить часто. В результате бывают случаи, когда вашему приложению может потребоваться отреагировать на определенное изменение в DOM.
Некоторое время общепринятым способом поиска изменений в DOM было использование функции Mutation Events, которая сейчас устарела. Одобренной W3C заменой Mutation Events является API MutationObserver, о котором я подробно расскажу в этой статье.
В ряде старых статей и ссылок обсуждается, почему старая функция была заменена, поэтому я не буду вдаваться в подробности здесь (за исключением того факта, что я не смог бы отдать должное). API MutationObserver
почти полностью поддерживается браузерами, поэтому мы можем безопасно использовать его в большинстве, если не во всех, проектах, если возникнет такая необходимость.
Базовый синтаксис для MutationObserver
MutationObserver
можно использовать разными способами, которые я подробно рассмотрю в оставшейся части этой статьи, но основной синтаксис для MutationObserver
выглядит следующим образом:
let observer = new MutationObserver(callback); function callback (mutations) { // do something here } observer.observe(targetNode, observerOptions);
Первая строка создает новый MutationObserver
с помощью конструктора MutationObserver()
. Аргумент, передаваемый в конструктор, представляет собой функцию обратного вызова, которая будет вызываться при каждом подходящем изменении DOM.
Способ определить, что подходит для конкретного наблюдателя, — с помощью последней строки в приведенном выше коде. В этой строке я использую observe()
объекта MutationObserver
, чтобы начать наблюдение. Вы можете сравнить это с чем-то вроде addEventListener()
. Как только вы прикрепите прослушиватель, страница будет «прослушивать» указанное событие. Точно так же, когда вы начнете наблюдать, страница начнет «наблюдение» за указанным MutationObserver
.
Метод observe()
принимает два аргумента: цель , которая должна быть узлом или деревом узлов, на котором следует наблюдать за изменениями; и объект параметров , который является объектом MutationObserverInit
, позволяющим определить конфигурацию наблюдателя.
Последней ключевой базовой функцией MutationObserver
является метод disconnect()
. Это позволяет прекратить наблюдение за указанными изменениями, и выглядит это так:
observer.disconnect();
Опции для настройки MutationObserver
Как уже упоминалось, observe()
для MutationObserver
требуется второй аргумент, указывающий параметры для описания MutationObserver
. Вот как будет выглядеть объект options со всеми возможными парами свойство/значение:
let options = { childList: true, attributes: true, characterData: false, subtree: false, attributeFilter: ['one', 'two'], attributeOldValue: false, characterDataOldValue: false };
При настройке параметров MutationObserver
нет необходимости включать все эти строки. Я включаю их просто для справки, чтобы вы могли видеть, какие параметры доступны и какие типы значений они могут принимать. Как видите, все, кроме одного, являются булевыми.
Чтобы MutationObserver
работал, по крайней мере один из childList
, attributes
или characterData
должен быть установлен в true
, иначе будет выдано сообщение об ошибке. Остальные четыре свойства работают в сочетании с одним из этих трех (подробнее об этом позже).
До сих пор я просто приукрашивал синтаксис, чтобы дать вам общее представление. Лучший способ понять, как работает каждая из этих функций, — предоставить примеры кода и живые демонстрации, включающие различные параметры. Так что это то, что я буду делать для остальной части этой статьи.
Наблюдение за изменениями в дочерних элементах с помощью childList
Первый и самый простой MutationObserver
, который вы можете инициировать, — это тот, который ищет дочерние узлы указанного узла (обычно элемента), которые нужно добавить или удалить. В моем примере я собираюсь создать неупорядоченный список в моем HTML, и я хочу знать, когда дочерний узел добавляется или удаляется из этого элемента списка.
HTML для списка выглядит следующим образом:
<ul class="list"> <li>Apples</li> <li>Oranges</li> <li>Bananas</li> <li class="child">Peaches</li> </ul>
JavaScript для моего MutationObserver
включает следующее:
let mList = document.getElementById('myList'), options = { childList: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'childList') { console.log('Mutation Detected: A child node has been added or removed.'); } } } observer.observe(mList, options);
Это только часть кода. Для краткости я показываю наиболее важные разделы, связанные с самим MutationObserver
API.
Обратите внимание, как я перебираю аргумент mutations
, который является объектом MutationRecord
, имеющим ряд различных свойств. В этом случае я читаю свойство type
и записываю сообщение о том, что браузер обнаружил подходящую мутацию. Кроме того, обратите внимание, как я передаю элемент mList
(ссылка на мой HTML-список) в качестве целевого элемента (то есть элемента, изменения которого я хочу отслеживать).
- Посмотреть полную интерактивную демонстрацию →
Используйте кнопки для запуска и остановки MutationObserver
. Сообщения журнала помогают прояснить, что происходит. Комментарии в коде также дают некоторые пояснения.
Здесь обратите внимание на несколько важных моментов:
- Функция обратного вызова (которую я назвал
mCallback
, чтобы проиллюстрировать, что вы можете назвать ее как угодно) будет срабатывать каждый раз, когда обнаруживается успешная мутация и после выполненияobserve()
. - В моем примере единственным «типом» мутации, который соответствует требованиям, является
childList
, поэтому имеет смысл искать его при циклическом просмотре MutationRecord. Поиск любого другого типа в этом экземпляре ничего не даст (другие типы будут использоваться в последующих демонстрациях). - Используя
childList
, я могу добавить или удалить текстовый узел из целевого элемента, и это тоже подходит. Таким образом, это не обязательно должен быть элемент, который добавляется или удаляется. - В этом примере подходят только непосредственные дочерние узлы. Позже в статье я покажу вам, как это можно применить ко всем дочерним узлам, внукам и так далее.
Наблюдение за изменениями атрибутов элемента
Другой распространенный тип мутации, которую вы, возможно, захотите отслеживать, — это изменение атрибута указанного элемента. В следующей интерактивной демонстрации я буду наблюдать за изменениями атрибутов элемента абзаца.
let mPar = document.getElementById('myParagraph'), options = { attributes: true }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } } observer.observe(mPar, options);
- Попробуйте демо →
Опять же, я сократил код для ясности, но важные части таковы:
- Объект
options
использует свойствоattributes
, для которого задано значениеtrue
, чтобы сообщитьMutationObserver
, что я хочу искать изменения в атрибутах целевого элемента. - Тип мутации, который я проверяю в своем цикле, —
attributes
, единственный, который подходит в данном случае. - Я также использую свойство
attributeName
объектаmutation
, которое позволяет мне узнать, какой атрибут был изменен. - Когда я запускаю наблюдатель, я передаю элемент абзаца по ссылке вместе с параметрами.
В этом примере кнопка используется для переключения имени класса в целевом элементе HTML. Функция обратного вызова в наблюдателе мутаций запускается каждый раз, когда класс добавляется или удаляется.
Наблюдение за изменениями данных персонажа
Еще одно изменение, которое вы, возможно, захотите найти в своем приложении, — это мутации символьных данных; то есть изменяется на конкретный текстовый узел. Это делается путем установки свойства characterData
в значение true
в объекте options
. Вот код:
let options = { characterData: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'characterData') { // Do something here... } } }
Обратите внимание, что type
, который ищется в функции обратного вызова, — characterData
.
- Посмотреть живую демонстрацию →
В этом примере я ищу изменения в конкретном текстовом узле, который я нацеливаю через element.childNodes[0]
. Это немного хакерски, но для этого примера подойдет. Текст редактируется пользователем с помощью атрибута contenteditable
в элементе абзаца.
Проблемы при наблюдении за изменениями данных персонажа
Если вы возились с contenteditable
, то, возможно, знаете, что существуют сочетания клавиш, которые позволяют редактировать форматированный текст. Например, CTRL-B делает текст полужирным, CTRL-I выделяет текст курсивом и так далее. Это разобьет текстовый узел на несколько текстовых узлов, поэтому вы заметите, что MutationObserver
перестанет отвечать, если вы не отредактируете текст, который все еще считается частью исходного узла.
Я также должен отметить, что если вы удалите весь текст, MutationObserver
больше не будет вызывать обратный вызов. Я предполагаю, что это происходит потому, что как только текстовый узел исчезает, целевой элемент больше не существует. Чтобы бороться с этим, моя демонстрация перестает наблюдать, когда текст удаляется, хотя при использовании ярлыков форматированного текста все становится немного липким.
Но не волнуйтесь, позже в этой статье я расскажу о лучшем способе использования параметра characterData
без необходимости иметь дело со многими из этих особенностей.

Наблюдение за изменениями указанных атрибутов
Ранее я показал вам, как отслеживать изменения атрибутов указанного элемента. В этом случае, хотя демонстрация вызывает изменение имени класса, я мог бы изменить любой атрибут указанного элемента. Но что, если я хочу наблюдать за изменениями одного или нескольких конкретных атрибутов, игнорируя другие?
Я могу сделать это, используя необязательное свойство attributeFilter
в объекте option
. Вот пример:
let options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'] }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
Как показано выше, свойство attributeFilter
принимает массив определенных атрибутов, которые я хочу отслеживать. В этом примере MutationObserver
будет запускать обратный вызов каждый раз, когда изменяется один или несколько атрибутов hidden
, contenteditable
или data-par
.
- Посмотреть живую демонстрацию →
Опять же, я нацелился на определенный элемент абзаца. Обратите внимание на раскрывающийся список выбора, который выбирает, какой атрибут будет изменен. Атрибут draggable
— единственный, который не подходит, поскольку я не указал его в своих параметрах.
Обратите внимание, что в коде я снова использую свойство attributeName
объекта MutationRecord
, чтобы регистрировать, какой атрибут был изменен. И, конечно же, как и в других демонстрациях, MutationObserver
не начнет отслеживать изменения, пока не будет нажата кнопка «Пуск».
Еще одна вещь, которую я должен указать здесь, это то, что в этом случае мне не нужно устанавливать значение attributes
в true
; это подразумевается из-за того, что для attributesFilter
установлено значение true. Вот почему мой объект опций может выглядеть следующим образом и работать так же:
let options = { attributeFilter: ['hidden', 'contenteditable', 'data-par'] }
С другой стороны, если бы я явно установил для attributes
значение false
вместе с массивом attributeFilter
, это не сработало бы, поскольку значение false
имело бы приоритет, а параметр фильтра был бы проигнорирован.
Наблюдение за изменениями узлов и их поддеревьев
До сих пор при настройке каждого MutationObserver
я имел дело только с самим целевым элементом и, в случае childList
, с непосредственными дочерними элементами элемента. Но, безусловно, может быть случай, когда я мог бы захотеть наблюдать за изменениями в одном из следующих:
- Элемент и все его дочерние элементы;
- Один или несколько атрибутов элемента и его дочерних элементов;
- Все текстовые узлы внутри элемента.
Все вышеперечисленное может быть достигнуто с помощью свойства subtree
объекта options.
дочерний список с поддеревом
Во-первых, давайте искать изменения в дочерних узлах элемента, даже если они не являются непосредственными дочерними элементами. Я могу изменить свой объект параметров, чтобы он выглядел так:
options = { childList: true, subtree: true }
Все остальное в коде более или менее такое же, как и в предыдущем примере с childList
, за исключением некоторых дополнительных элементов разметки и кнопок.
- Посмотреть живую демонстрацию →
Здесь есть два списка, один вложен в другой. Когда MutationObserver
запускается, обратный вызов срабатывает при изменении любого из списков. Но если бы я изменил свойство subtree
обратно на false
(по умолчанию, когда оно отсутствует), обратный вызов не выполнялся бы при изменении вложенного списка.
Атрибуты с поддеревом
Вот еще один пример, на этот раз с использованием subtree
с attributes
и attributeFilter
. Это позволяет мне наблюдать за изменениями атрибутов не только целевого элемента, но и атрибутов любых дочерних элементов целевого элемента:
options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'], subtree: true }
- Посмотреть живую демонстрацию →
Это похоже на предыдущую демонстрацию атрибутов, но на этот раз я настроил два разных элемента select. Первый изменяет атрибуты целевого элемента абзаца, а другой изменяет атрибуты дочернего элемента внутри абзаца.
Опять же, если бы вы вернули параметру subtree
значение false
(или удалили его), вторая кнопка-переключатель не вызвала бы обратный вызов MutationObserver
. И, конечно же, я мог вообще не использовать attributeFilter
, и MutationObserver
искал бы изменения любых атрибутов в поддереве, а не в указанных.
characterData С поддеревом
Помните, в более ранней демонстрации characterData
были некоторые проблемы с исчезновением целевого узла, а затем MutationObserver
больше не работал. Хотя есть способы обойти это, проще нацелиться на элемент напрямую, а не на текстовый узел, а затем использовать свойство subtree
, чтобы указать, что я хочу, чтобы все символьные данные внутри этого элемента, независимо от того, насколько глубоко он вложен, запускались. обратный вызов MutationObserver
.
Мои варианты в этом случае будут выглядеть так:
options = { characterData: true, subtree: true }
- Посмотреть живую демонстрацию →
После запуска обозревателя попробуйте использовать CTRL-B и CTRL-I для форматирования редактируемого текста. Вы заметите, что это работает намного эффективнее, чем в предыдущем примере с characterData
. В этом случае разбитые дочерние узлы не влияют на наблюдателя, потому что мы наблюдаем за всеми узлами внутри целевого узла, а не за одним текстовым узлом.
Запись старых значений
Часто, наблюдая за изменениями в DOM, вы захотите принять к сведению старые значения и, возможно, сохранить их или использовать в другом месте. Это можно сделать, используя несколько различных свойств в объекте options
.
атрибутOldValue
Во-первых, давайте попробуем удалить старое значение атрибута после его изменения. Вот как мои параметры будут выглядеть вместе с моим обратным вызовом:
options = { attributes: true, attributeOldValue: true } function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
- Посмотреть живую демонстрацию →
Обратите внимание на использование свойств attributeName
и oldValue
объекта MutationRecord
. Попробуйте демо, введя разные значения в текстовое поле. Обратите внимание, как журнал обновляется, чтобы отразить предыдущее значение, которое было сохранено.
характерДанныеСтароеЗначение
Точно так же вот как будут выглядеть мои параметры, если я хочу регистрировать старые данные персонажа:
options = { characterData: true, subtree: true, characterDataOldValue: true }
- Посмотреть живую демонстрацию →
Обратите внимание, что в сообщениях журнала указано предыдущее значение. Все становится немного шатким, когда вы добавляете в смесь HTML с помощью команд форматированного текста. Я не уверен, каким должно быть правильное поведение в этом случае, но более просто, если внутри элемента находится только один текстовый узел.
Перехват мутаций с помощью takeRecords()
Еще один метод объекта MutationObserver
, о котором я еще не упоминал, — это takeRecords()
. Этот метод позволяет более или менее перехватывать обнаруженные мутации до того, как они будут обработаны функцией обратного вызова.
Я могу использовать эту функцию, используя следующую строку:
let myRecords = observer.takeRecords();
Это сохраняет список изменений DOM в указанной переменной. В моей демонстрации я выполняю эту команду, как только нажимается кнопка, которая изменяет DOM. Обратите внимание, что кнопки «Пуск» и «Добавить/удалить» ничего не регистрируют. Это связано с тем, что, как уже упоминалось, я перехватываю изменения DOM до того, как они будут обработаны обратным вызовом.
Однако обратите внимание, что я делаю в прослушивателе событий, который останавливает наблюдателя:
btnStop.addEventListener('click', function () { observer.disconnect(); if (myRecords) { console.log(`${myRecords[0].target} was changed using the ${myRecords[0].type} option.`); } }, false);
Как видите, после остановки наблюдателя с observer.disconnect()
я получаю доступ к перехваченной записи мутации и регистрирую целевой элемент, а также тип записанной мутации. Если бы я наблюдал за несколькими типами изменений, то в сохраненной записи было бы более одного элемента, каждый со своим типом.
Когда запись мутации перехватывается таким образом с помощью вызова takeRecords()
, очередь мутаций, которая обычно отправляется в функцию обратного вызова, очищается. Поэтому, если по какой-то причине вам нужно перехватить эти записи до их обработки, takeRecords()
.
Наблюдение за несколькими изменениями с помощью одного наблюдателя
Обратите внимание: если я ищу мутации в двух разных узлах на странице, я могу сделать это, используя один и тот же наблюдатель. Это означает, что после вызова конструктора я могу выполнить observe()
для любого количества элементов.
Таким образом, после этой строки:
observer = new MutationObserver(mCallback);
Затем я могу иметь несколько observe()
с разными элементами в качестве первого аргумента:
observer.observe(mList, options); observer.observe(mList2, options);
- Посмотреть живую демонстрацию →
Запустите наблюдатель, затем попробуйте добавить/удалить кнопки для обоих списков. Единственная загвоздка здесь в том, что если вы нажмете одну из кнопок «стоп», наблюдатель перестанет наблюдать за обоими списками, а не только за тем, на который он нацелен.
Перемещение наблюдаемого дерева узлов
Последнее, на что я укажу, это то, что MutationObserver
будет продолжать наблюдать за изменениями в указанном узле даже после того, как этот узел будет удален из его родительского элемента.
Например, попробуйте следующую демонстрацию:
- Посмотреть живую демонстрацию →
Это еще один пример использования childList
для отслеживания изменений в дочерних элементах целевого элемента. Обратите внимание на кнопку, которая отключает подсписок, за которым наблюдают. Нажмите кнопку «Пуск…», затем нажмите кнопку «Переместить…», чтобы переместить вложенный список. Даже после того, как список удален из его родителя, MutationObserver
продолжает наблюдать за указанными изменениями. Неудивительно, что это происходит, но об этом нужно помнить.
Заключение
Это охватывает почти все основные функции MutationObserver
API. Я надеюсь, что это глубокое погружение было полезным для вас, чтобы ознакомиться с этим стандартом. Как уже упоминалось, поддержка браузеров надежна, и вы можете узнать больше об этом API на страницах MDN.
Я поместил все демонстрации для этой статьи в коллекцию CodePen, если вы хотите иметь удобное место для работы с демонстрациями.