MutationObserver и MutationEvent
Дата публикации: 22.01.2014
Часто бывают ситуации, когда при динамической вставке элемента в 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
Даже при таком оптимизированном способе вставки элементов на страницу, при использовании 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();
}());
Пример в действии
http://jsfiddle.net/valsie/dtqR2/1/
Какими браузерами поддерживается http://caniuse.com/#feat=mutationobserver
Для тех браузеров, которые не поддерживают эту технологию, можно использовать MutationEvent или иные приемы, к примеру, выполнять с определенным интервалом проверку на наличие новых элементов в наблюдаемом узле DOM.
