MutationObserver и MutationEvent
Часто бывают ситуации, когда при динамической вставке элемента в DOM, или изменении атрибута элемента, необходимо выполнить с этим элементом некоторые действия. Например, это может быть запуск сценария для конвертации стандартных элементов ввода в нестандартные (checkbox, radio кнопок, выпадающих списков, загрузчика файлов, и т.д.), навешивание событий на новые элементы. В современных веб-приложениях это дает особую пользу, в том случае когда нам неизвестно, какое содержимое будет вставлено в DOM, когда данные приходят с сервера используя AJAX.
Ранее, для того чтобы вести контроль над тем, какие данные были вставлены в DOM, необходимо было выполнять поиск по содержимому, проводить различные проверки DOM элементов, что накладывало собой большую нагрузку на производительность. Вдобавок к этому, нужно было учитывать в каких местах кода, есть необходимость выполнять эти действия, особенно если над проектом параллельно работает несколько человек - это накладывает определенные сложности и нарушает чистоту кода.
Современные браузеры поддерживают инструменты, с помощью которых предоставляется возможность реагировать на изменения DOM.
Первый из таких инструментов, это MutationEvents, интерфейс которого описан в спецификации DOM2, в котором определено несколько событий, которые срабатывают при модификации DOM.
Событие | Описание |
DOMSubtreeModified | Это событие уведомляет о всех изменениях элементов документа. Он может быть использован вместо более конкретных событий, перечисленных ниже. |
DOMNodeInserted | Срабатывает при вставке элементов в DOM. |
DOMNodeRemoved | Срабатывает при удалении DOM элемента. |
DOMNodeRemovedFromDocument | Срабатывает, когда элемент удаляется из документа, либо путем прямого удаления узла или удаления поддерева, в котором он содержится. Это событие срабатывает перед удалением. Событие DOMNodeRemoved срабатывает до начала этого события. |
DOMNodeInsertedIntoDocument | Это событие срабатывает после того как в документв вставлен элемент. Событие DOMNodeInserted срабатывает раньше этого события. |
DOMAttrModified | Срабатывает, когда выполняется изменение атрибутов элемента DOM. |
DOMCharacterDataModified | Срабатывает, когда выполняется модификация текстового элемента DOM. |
Однако использование мутационных событий имеет некоторые проблемы.
Проблемы связанные с MutationEvents
Идея таких мутационных событий хороша в теории, но на практике появились две основные проблемы:
- MutationEvents синхронны. События запускаются при их вызове, и могут предотвращать вызов других событий, которые находятся в очереди. Так же добавление и удаление узлов, может привести к замедлению или зависанию приложения.
- Потому, что это события, и реализована эта технология как события. События могут срабатывать, а иногда и всплывать. При срабатывании этих событий или всплытии, могут происходить изменения в DOM, что может способствовать повторному срабатыванию MutationEvents - и такое поведение может привести к полному зависанию браузера.
В итоге, получается, что события мутации DOM достаточно запутаны, по этому они не рекомендуются в DOM Level 3 спецификации. Но если события мутации являются устаревшими, нам нужно что-то, чтобы заменить их. На замену событий к нам приходит MutationObserver
MutationObserver - предоставляет разработчикам возможность реагировать на изменения в DOM. Он предназначен в качестве замены для события типа MutationEvent, определенных в спецификации событий DOM2.
В чем заключается разница между MutationEvents и MutationObserver
MutationObserver определяется в стандарте DOM, и отличаются от MutationEvents одним ключевым свойством - MutationObserver является асинхронными. Он не срабатывает каждый раз, когда происходит событие. Вместо этого:
- Ожидает окончания других сценариев или задач.
- Сообщает об изменении в виде массива мутаций а не один за одним.
- Можно наблюдать все изменения в элементы, или только отдельные.
Более того, поскольку MutationObserver не является событием, по этому не накладывает нагрузку на систему событий, а так же менее вероятно может затормозить UI или вызвать "падение" браузера.
Давайте рассмотрим пример. В приведенном ниже коде, мы добавляя 2500 параграфов к фрагменту документа, а затем добавим фрагмент в документ.
var docFrag = document.createDocumentFragment(), thismany = 2500, i=0, a = document.querySelector("article"), p; while ( i < thismany) { // Creates a new p element if one doesn't exists. // Clones the existing element if it does. p = (p === undefined) ? document.createElement("p") : p.cloneNode(false); docFrag.appendChild(p); i++; } a.appendChild( docFrag );
Даже при таком оптимизированном способе вставки элементов на страницу, при использовании MutationEvents, произойдет генерация 2500 DOMNodeInserted событий, по одному на каждый параграф. В случае использования MutationObserver, функция обратного вызова будет вызвана только один раз, и будет содержать все 2500 элементов.
Для начала работы с MutationObserver необходимо выполнить инициализацию с одним параметром, функцией обратного вызова, которая будет срабатывать при изменении DOM.
var observer = new MutationObserver(function (mutations) { // выполнение требуемых действий });
Инстанцированный объект имеет три метода:
Наименование | Описание |
observe | Регистрирует экземпляр объекта MutationObserver получать уведомления о DOM мутаций на указанном узле, иными словами - выполняет запуск работы прослушивания изменений в DOM. |
disconnect | Останавливает экземпляр объекта MutationObserver от получения уведомлений о DOM мутациях. |
takeRecords | Очищает очередь записи экземпляра MutationObserver и возвращает то, что было там. |
При старте прослушивания, функция observe принимает два параметра, первый - DOM элемент изменения в котором будут наблюдаться, и второй параметр, который является настройками отвечающими за сам процесс наблюдения, представляет из себя объект со следующими свойствами:
Свойство | Описание |
childList | Устанавливается в true, что бы наблюдать за дочерними узлами внутри заданного контейнера |
attributes | Устанавливается в true, что бы наблюдать за атрибутами дочерних элементов заданного контейнера |
characterData | Устанавливается в true, что бы наблюдать за изменениями текстовых DOM элементов. |
subtree | По умолчанию, объект наблюдает только за элементам верхнего уровня, заданного контейнера, для того что бы отслеживать мутации во всех уровнях внутри контейнера, нужно установить этот параметр в true. |
attributeOldValue | Необходимо установить параметр в true, что бы значение атрибута перед мутацией было записано |
characterDataOldValue | Необходимо установить параметр в true, что бы значение текстового элемента перед мутацией было записано |
attributeFilter | Устанавливается массив атрибутов, на которые не должен реагировать наблюдатель |
При изменении узла в DOM элементе наблюдаемого контейнера, в функцию обратного вызова передается массив записей. Каждая запись состоит из таких атрибутов:
Наименование | Описание |
type | Содержит наименование типа изменяемого значения: childList, attributes, characterData |
target | Возвращает DOM узел, на которых повешен наблюдатель |
addedNodes | Возвращает массив добавленных узлов, или null |
removedNodes | Возвращает массив удаленных узлов, или null |
previousSibling | Возвращает предыдущий соседний узел из добавленных или удаленных узлов, или null. |
nextSibling | Возвращает следующий соседний узел из добавленных или удаленных узлов, или null. |
attributeName | Возвращает имя измененного атрибута, или null. |
attributeNamespace | Возвращает пространство имен измененного атрибута, или null. |
oldValue | Возвращает значение, зависящие от типа. Для attribute - значение измененного атрибута до изменения. Для CharacterData, это данные измененной узла до изменения. Для ChildList, он является недействительным. |
Пример использования
/** * Mutation observer. Test application * * @module MutationObserverTest */ (function MutationObserverTest() { "use strict"; var /** * Contains main objects * * @property controls */ controls = {}, /** * Contains event handlers * * @property handlers */ handlers = { /** * Add new element to document * * @method addElementClick */ addElementClick: function () { var div = document.createElement("div"), newId = parseInt((Math.random() * 1000)).toString(); div.setAttribute("data-name", "element"); div.innerHTML = '<label for=" + newId + "> </label>' + '<input id=" + newId + " type="checkbox" name="some-check">' + '<input type="text" name="some-text">'; controls.content.appendChild(div); }, /** * triggered when click by remove button * * @method removeElementClick */ removeElementClick: function () { var elementForRemove = controls.content.querySelectorAll("input:checked"), elementsCount = elementForRemove.length, i; for (i = 0; i < elementsCount; i++) { controls.content.removeChild(elementForRemove[i].parentNode); } }, /** * Mutation event * * @method domSubtreeModified */ domSubtreeModified: function (event) { console.log("Event: DOMSubtreeModified"); }, /** * Mutation event * * @method domNodeInserted */ domNodeInserted: function () { console.log("Event: DOMNodeInserted"); }, /** * Mutation event * * @method domNodeRemove */ domNodeRemove: function () { console.log("Event: DOMNodeRemove"); }, /** * Mutation event * * @method domAttModified */ domAttModified: function () { console.log("Event: DOMAttModified"); }, /** * Mutation event * * @method domCharacterDataModified */ domCharacterDataModified: function () { console.log("Event: DOMCharacterDataModified"); }, /** * Mutation event * * @method domNodeRemovedFromDocument */ domNodeRemovedFromDocument: function () { console.log("Event: DOMNodeRemovedFromDocument"); }, /** * Mutation event * * @method domNodeInsertedIntoDocument */ domNodeInsertedIntoDocument: function () { console.log("Event: DOMNodeInsertedIntoDocument"); }, /** * Mutation observer callback event * * @method mutationObserverCallback */ mutationObserverCallback: function (mutations) { var mutationCount = mutations.length, i, currentMutation, monitorMessage; for (i = 0; i < mutationCount; i++) { currentMutation = mutations[i]; monitorMessage = document.createElement("div"); monitorMessage.innerHTML = "MutationObserver - " + currentMutation.type; controls.monitor.appendChild(monitorMessage); switch (currentMutation.type) { case "childList": if (currentMutation.addedNodes.length) { currentMutation.addedNodes[0].setAttribute("name", "page element"); currentMutation.addedNodes[0].setAttribute("data-name", "page element"); } else if (currentMutation.removedNodes.length) { console.log("MutationObserver - remove node"); } break; case "attributes": currentMutation.target.querySelector("label").innerText = currentMutation.target.getAttribute(currentMutation.attributeName); break; case "characterData": console.log("MutationObserver - " + currentMutation.target.nodeValue); break; } } } }; // Выбираем наблюдаемый элемент var target = document.querySelector("#some-id"); // Выполняем инстанцирование наблюдателя var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { console.log(mutation.type); }); }); // Конфигурация наблюдателя var config = { attributes: true, childList: true, characterData: true }; // Устанавливаем налюдаемый узел и конфигурацию налюдения observer.observe(target, config); // Через некоторое время, мы можем остановить наблюдение observer.disconnect();
Теперь рассмотрим более сложный пример, с добавлением элементов, удалением, изменением атрибутов, а так же накладыванием фильтра на реагирование изменения некоторых атрибутов. Так же для примера можем пронаблюдать работу MutationEvents, как ведут себя события одновременно с работой MutationObserver.
Что бы не нагромождать интерфейс примера, часть информации о событиях и работе наблюдателя выводится в консоль.
<!DOCTYPE html> <html> <head> <title></title> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> <style type="text/css"> .main-wrap { width: 600px; } .main-panel__add, .main-panel__remove { color: blue; text-decoration: underline; cursor: pointer; } .main-panel__add:hover { text-decoration: none; } .monitor { float: right; width: 200px; } .content { width: 400px; } </style> </head> <body> <!-- main wrap --> <div id="main-wrap" class="main-wrap"> <!-- main panel --> <div class="main-panel"> <span class="main-panel__add">Добавить элемент</span> <span class="main-panel__remove">Удалить выбранный элемент</span> </div> <!--/main panel --> <!-- monitor --> <aside class="monitor"></aside> <!--/monitor --> <!-- content --> <div class="content"></div> <!--/content --> </div> <!--/main wrap --> <script src="js/main.js"></script> </body> </html>
/** * Mutation observer. Test application * * @module MutationObserverTest */ (function MutationObserverTest() { 'use strict'; var /** * Contains main objects * * @property controls */ controls = {}, /** * Contains event handlers * * @property handlers */ handlers = { /** * Add new element to document * * @method addElementClick */ addElementClick: function () { var div = document.createElement("div"), newId = parseInt((Math.random() * 1000)).toString(); div.setAttribute("data-name", "element"); div.innerHTML = '<label for=" + newId + "> </label>' + '<input id=" + newId + " type="checkbox" name="some-check">' + '<input type="text" name="some-text">'; controls.content.appendChild(div); }, /** * triggered when click by remove button * * @method removeElementClick */ removeElementClick: function () { var elementForRemove = controls.content.querySelectorAll("input:checked"), elementsCount = elementForRemove.length, i; for (i = 0; i < elementsCount; i++) { controls.content.removeChild(elementForRemove[i].parentNode); } }, /** * Mutation event * * @method domSubtreeModified */ domSubtreeModified: function (event) { console.log("Event: DOMSubtreeModified"); }, /** * Mutation event * * @method domNodeInserted */ domNodeInserted: function () { console.log("Event: DOMNodeInserted"); }, /** * Mutation event * * @method domNodeRemove */ domNodeRemove: function () { console.log("Event: DOMNodeRemove"); }, /** * Mutation event * * @method domAttModified */ domAttModified: function () { console.log("Event: DOMAttModified"); }, /** * Mutation event * * @method domCharacterDataModified */ domCharacterDataModified: function () { console.log("Event: DOMCharacterDataModified"); }, /** * Mutation event * * @method domNodeRemovedFromDocument */ domNodeRemovedFromDocument: function () { console.log("Event: DOMNodeRemovedFromDocument"); }, /** * Mutation event * * @method domNodeInsertedIntoDocument */ domNodeInsertedIntoDocument: function () { console.log("Event: DOMNodeInsertedIntoDocument"); }, /** * Mutation observer callback event * * @method mutationObserverCallback */ mutationObserverCallback: function (mutations) { var mutationCount = mutations.length, i, currentMutation, monitorMessage; for (i = 0; i < mutationCount; i++) { currentMutation = mutations[i]; monitorMessage = document.createElement("div"); monitorMessage.innerHTML = "MutationObserver - " + currentMutation.type; controls.monitor.appendChild(monitorMessage); switch (currentMutation.type) { case "childList": if (currentMutation.addedNodes.length) { currentMutation.addedNodes[0].setAttribute("name", "page element"); currentMutation.addedNodes[0].setAttribute("data-name", "page element"); } else if (currentMutation.removedNodes.length) { console.log("MutationObserver - remove node"); } break; case "attributes": currentMutation.target.querySelector("label").innerText = currentMutation.target.getAttribute(currentMutation.attributeName); break; case "characterData": console.log("MutationObserver - " + currentMutation.target.nodeValue); break; } } } }; /** * get page controls * * @method getControls */ function getControls() { var mainWrap = document.getElementById("main-wrap"); controls.mainWrap = mainWrap; controls.content = mainWrap.querySelector(".content"); controls.monitor = mainWrap.querySelector(".monitor"); } /** * add event listener * * @method addEventList */ function addEventList() { $(controls.mainWrap).on("click", ".main-panel__add", handlers.addElementClick); $(controls.mainWrap).on("click", ".main-panel__remove", handlers.removeElementClick); controls.content.addEventListener("DOMSubtreeModified", handlers.domSubtreeModified, false); controls.content.addEventListener("DOMNodeInserted", handlers.domNodeInserted, false); controls.content.addEventListener("DOMNodeRemoved", handlers.domNodeRemove, false); controls.content.addEventListener("DOMAttrModified", handlers.domAttModified, false); controls.content.addEventListener("DOMCharacterDataModified", handlers.domCharacterDataModified, false); controls.content.addEventListener("DOMNodeRemovedFromDocument", handlers.domNodeRemovedFromDocument, false); controls.content.addEventListener("DOMNodeInsertedIntoDocument", handlers.domNodeInsertedIntoDocument, false); } function initMutationObserver() { var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver, observer; observer = new MutationObserver(handlers.mutationObserverCallback); observer.observe(controls.content, { childList: true, attributes: true, characterData: true, subtree: true, attributeOldValue: true, characterDataOldValue: true, attributeFilter: ["data-name"] }); } /** * init function * * @method init */ function init() { getControls(); addEventList(); initMutationObserver(); } return init(); }());
Пример в действии
Какими браузерами поддерживается
Для тех браузеров, которые не поддерживают эту технологию, можно использовать MutationEvent или иные приемы, к примеру, выполнять с определенным интервалом проверку на наличие новых элементов в наблюдаемом узле DOM.