DOM MutationObserver — реакция на изменение DOM не убивая производительность браузера

https://hacks.mozilla.org/2012/05/dom-mutationobserver-reacting-to-dom-changes-without-killing-browser-performance/
  • Перевод
DOM Mutation Events в свое время казались отличной идеей — веб-разработчики начали создавать более динамичные приложения, и казалась естественной та радость с которой были встречены новые возможности прослушивать изменения DOM и реагировать на них. На практике, однако, оказалось, что у DOM Mutation Events имеются серьезные проблемы с производительностью и стабильностью. Не удивительно, что спецификация через год получила статус “устаревшей”.

Но сама идея, лежащая в основе DOM Mutation Events казалась привлекательной и поэтому в сентябре 2011 г. группа инженеров Google и Mozilla представила предложение о DOM MutationObserver, с похожей функциональностью, но улучшенной производительностью. Это новое DOM-API доступно начиная с версий: Firefox 14, Chrome 18, IE 11, Safari 6 (остальные браузеры — caniuse.com/mutationobserver)

В простейшем случае MutationObserver используется примерно так:

// выбираем элемент
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();


Ключевым преимуществом новой спецификации перед устаревшей DOM Mutation Events является эффективность. При наблюдении за узлом DOM, ваша функция обратного вызова не сработает до тех пор, пока изменения DOM полностью не завершатся. Когда же callback сработает, в него будет передан итоговый список изменений DOM, пробежавшись по которому можно будет как-то отреагировать на изменения.

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

<!DOCTYPE html>
<ol contenteditable oninput="">
  <li>Press enter</li>
</ol>
<script>
  var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
  var list = document.querySelector('ol');
 
  var observer = new MutationObserver(function(mutations) {  
    mutations.forEach(function(mutation) {
      if (mutation.type === 'childList') {
        var list_values = [].slice.call(list.children)
            .map( function(node) { return node.innerHTML; })
            .filter( function(s) {
              if (s === '<br>') {
                return false;
              }
              else {
                return true;
              }
        });
        console.log(list_values);
      }
    });
  });
 
  observer.observe(list, {
  	attributes: true, 
  	childList: true, 
  	characterData: true 
   });
</script>


Для того, чтобы вы могли посмотреть код в деле, я разместил его на jsbin:

jsbin.com/ivamoh/53/edit

Если вы поигрались с кодом в песочнице, вы, наверное, отметили одну особенность: функция обратного вызова срабатывает только тогда, когда вы нажимаете “ввод” на каком-либо элементе списка — по существу, это из-за того, что ваши действия приводят к добавлению или удалению узла DOM. Это важнейшее отличие от других техник, таких как подписка на нажатие клавиш, или клики мышью. MutationObservers работает не так, как все эти техники — здесь срабатывание происходит только при изменении DOM, а не как реакция на события, инициируемые JS, или действиями пользователей.

Итак, а где же это использовать?

Не думаю, что все программисты тут же ни с того ни с сего, все побросают, и начнут встраивать MutationObservers в свой код. Вероятно больше всего данный API подойдет разработчикам JS-фреймворков, для реализации чего-то нового, или того, что не было раньше реализовано из-за проблем с производительностью. Так же данная техника пригодиться, если вы используете какой-либо фреймворк, изменяющий DOM, и вам необходимо реагировать на эти изменения (без использования костылей, типа setTimeout). Ну и последнее — это разработка браузерных расширений.

Ресурсы:


От переводчика: материалу больше года, но что-то уж больно мало на русском о MutationObserver — может кому пригодится.
Поделиться публикацией

Комментарии 17

    +4
    Как раз на прошлых выходных пользовался MutationSummary в рамках хаккатона. Хорошая вещь, но разочарований больше, чем радостей. На видео один из разработчиков показывает DOM-зеркало: на одной странице поисходят изменения, а в другой они полностью повторяются. На практике взять код «как есть» не удалось, т.к. он предполагает, что доступ к оригиналу возможен через DOM API — либо это фреймы на одной странице, либо как в примере — связь через расширение браузера. Мы же хотели «достучаться» до странице на другом устройстве, и к сожалению, так «зеркало» не работает.

    А настраивать mutation summary руками оказалось тоже неприятно. Если у вас простой случай — например, текст внутри элемента меняется или меняются атрибуты — то все замечательно. Мы же работали с rich-текстом через contentEditable, и там простое изменение часто влечет к появлению 5--6 «мутаций» одновременно.

    В итоге остались баги, которые поправить вовремя не удалось. И как следствие, низкие оценки :(
      0
      Может проблема была в библиотеке, а не в самих Observer-ах?
        +1
        Надеюсь вы доделаете все до конца и расскажете здесь, в чем же было дело.
          +2
          Эффективно связать две страницы в рамках одного браузера, если они не кросдоменны, можно посредством localStorage и ивента onstorage. Вся соль этого ивента, что он не срабатывает на странице, которая непосредственно произвела работу со стореджем, а на других страницах этого же сайта, открытых в этом же браузере — срабатывает.

          upd: извините, не заметил, что вам надо было достучаться до страницы на другом устройстве.
            +1
            А MutationObserver из Polymer не пробовали?
              0
              Нет, не пробовал, хотя с Полимером работаю. Посмотрю, спасибо за наводку.
              +1
              Кстати, забавный факт. Авторы MutationSummary — сотрудники компании Google, работающие над Chromium. Библиотека написана на TypeScript, созданным в Microsoft.
              +1
              Написал свой велосипед и радостно его использую в облачном плеере Listen! для Google Chrome, удобство радует неимоверно. При изменении DOM подписываюсь на созданные ноды + удаляю обработчики с удаленных.
                +3
                Пожалуйста, не надо использовать mutationObserver в продакшне, не в рамках библиотек. Он реально очень низкоуровневый.
                В чем, собственно, дело: по сути единственный способ изменить дом-дерево это код. Код, который написал разработчик.
                Если разработчик не способен архитектурно построить код так, чтобы реагировать на внесенные этим же кодом изменения в дом-дерево — я бы не назвал разработчика грамотным.
                Пользователь максимум может взаимодействовать с value инпутов и :checked у чекбокса — это из изменений структуры. Ну ладно, сейчас появились drag-n-drop события, но они тоже обрабатываются на жс, нельзя просто так все править, без создания базы для кода. Так что на исключение не тянет. Ладно, если говорить совсем откровенно — есть и contenteditable, но он до сих пор не кроссплатформенный, да и я честно еще не встречал его нигде, и, подозреваю, нескоро встречу.

                Mutation events необходимы для веб-компонентов, без них невозможно построить триггеры на добавление кастомного элемента в дом-дерево, что лишает их большинства преимуществ. Mozilla и Google вместе разрабатывали этот стандарт, и, конечно же, им хотелось сделать для себя разумные и простые решения. Кроме этого единственное, где реально имеет смысл использовать MutationObserver не в качестве очень грязного хака — это библиотеки, переводящие большую часть кода в декларативное состояние, например, Angular и ему подобные с директивами.

                А так — не надо пользовать. Не только даже потому что это почти невозможно отлаживать, но еще и потому что у MutationObserver до конца не выясненные глюки. Например, мало кому известно, что в хроме(во всяком случае раньше было так) на четвертом или пятом колбэке подряд, когда в самом колбеке редактируется содержимое элемента, иногда обсервер захлебывается и начинает пропускать события. При этом этот эффект наблюдается только на ванилле, jquery как-то его обходит.
                  +1
                  Если разработчик не способен архитектурно построить код так, чтобы реагировать на внесенные этим же кодом изменения в дом-дерево — я бы не назвал разработчика грамотным.

                  Не очень понятно почему. Логично, по-моему, использовать для реакции на изменения DOM события, а для их обработки паттерн «Наблюдатель». И как-то не совсем логично реализовывать свой механизм параллельно имеющемуся нативному.
                    0
                    В чем, собственно, дело: по сути единственный способ изменить дом-дерево это код. Код, который написал разработчик.

                    Не всегда. Иногда код встраивается в страницу и должен следить за изменениями на ней.
                    Например, мы в RedHelper довольно успешно используем MutationObserver для cobrowsing'а. Так что в продакшене использовать можно, а иногда даже нужно.
                      0
                      Что есть cobrowsing? Это когда кто-то подглядывает за тем, что происходит в браузере другого?
                        0
                        Это реализация на уровне библиотеки все же, и скорее всего вы ее довольно долго писали, и выявили кучу интересных особенностей поведения. Я говорил про конечных разработчиков, которые не хотят писать нужные вызовы на внесения правок в верстку, и вместо этого пользуют мутейшнобсервер.
                        0
                        contentEditable — очень распространенная штука. Его поведение по умолчанию — да, не кроссплатформенное, — но его можно «причесать».

                        Twitter и Google+, например, использует contentEditable для поля новых постов. В Outlook.com поле ввода текста письма — тоже contentEditable. В Gmail тоже так было. Первые версии Google Docs и других онлайн-редакторов были или остаются на contentEditable. Куча платформ для блогов и CMS тоже его используют.

                        Так что вы его наверняка встречали.
                        0
                        Крутая штука, использовал в плагинчике, весьма успешно. Упрощает API в разы: не нужны всякие методы add, etc, с DOM можно работать напрямую, а плагин возьмет всю работу на себя.

                        Проблема в том, что под старые браузеры (оперу и IE в первую очередь) надо писать жуткий фолбэк в виде сначала обработки событий DOMNodeChanged, DOMNodeRemoved, ..., а затем и вообще реализуя соответствующие методы.
                        Хотя сейчас ситуация уже получше: caniuse.
                          0
                          MutationObserver — штука прикольная, но к сожалению им нельзя отловить reflow. Предположим, я хочу отследить позицию элемента на странице, она может измениться, например из-за удаления соседней ноды, к сожалению, решить эту проблему не навешивая MutationObserver на каждый элемент либо без регулярной проверки в setInterval, у меня не получилось. Если кто-нибудь подскажет красивое решение, буду благодарен.
                            0
                            ну как бы можно повесить на body с атрибутом subtree — будет эмиттится на любое изменение.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое