Статьи

Эволюция новой мутации

Раньше я был большим поклонником DOM Mutation Events . Они предоставили уникальный сценарий для отслеживания изменений в DOM , независимо от события или действия, которые их вызвали. Таким образом, такие события, как DOMNodeInserted и DOMAttrModified будут DOMAttrModified в ответ на добавление узлов или изменения атрибутов (соответственно).

Но если вы никогда не использовали мутационные события, это не удивительно, поскольку большую часть времени вы сами добавляете эти узлы или изменяете эти атрибуты, и зачем вам реактивное событие для чего-то, что вы вызвали в первую очередь?

Таким образом, они в основном использовались для решения проблем в библиотеках и средах, например, для реагирования на изменения, происходящие от анонимных замыканий. Кроме того, они были довольно распространенным товаром для многих расширений браузера , где они предоставляли самый простой, а иногда и единственный способ определить, когда документ изменяется.

Синтаксис был очень прост, как и любое другое событие:

 element.addEventListener('DOMNodeInserted', function(e) { console.log('Added ' + e.target.nodeName + ' to ' + element.nodeName); }, false); 

Однако эта простота скрывала основную проблему — мутационные события не были реализованы должным образом, и они мешали разработке браузеров проблемами с производительностью и стабильностью. Они запускаются слишком часто, их медленно и сложно оптимизировать, и они являются источником любого количества потенциальных ошибок при сбое.

Вот почему события мутации осуждаются уже около двух лет, и надстройкам Firefox формально больше не разрешается их включать. На самом деле, когда я выпустил обновление для Dust-Me Selectors в прошлом году, мне пришлось попросить специальное разрешение на их дальнейшее использование!

Обратите внимание, что DOMContentLoaded не является событием мутации, оно просто имеет похожий тип имени. С этим событием таких проблем нет, и его использование не приветствуется.

Вы не можете положить хорошую идею

Несмотря на эти проблемы, идея событий мутации оставалась хорошей, и вскоре разработчики из Mozilla и Google собрали новое предложение , которое вскоре стало частью спецификации DOM 4 .

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

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

 var watcher = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { for(var i = 0; i < mutation.addedNodes.length; i ++) { console.log('Added ' + mutation.addedNodes[i].nodeName + ' to ' + mutation.target.nodeName); } }); }); 

Обратному вызову наблюдателя передается объект с данными о мутациях, каждый член которого представляет одно изменение. Это отличается от событий мутации, которые запускают обратный вызов отдельно для каждого изменения!

Данные, содержащиеся в каждом объекте мутации, зависят от того, что наблюдается. В этом случае мы наблюдаем только за изменениями дочерних элементов целевого элемента (указанных параметром childList в объекте конфигурации), и поэтому объект мутации имеет свойство addedNodes , которое представляет собой коллекцию ссылок на каждый из добавленных узлов.

Вот демонстрация этого примера, которая работает в Firefox 14 или более поздней версии и Chrome 18 или более поздней версии:

В демоверсии есть кнопка, которую вы можете нажать, чтобы добавить новый абзац на страницу, и каждый раз, когда это происходит, наблюдатель будет отвечать. Конечно, на практике вы бы этого не делали — вы бы просто использовали событие click для запуска чего бы то ни было — но дело в том, что наблюдатель может реагировать на изменения, вызванные чем-либо — включая (и особенно) сценарии, которых у вас нет другой контроль над.

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

Некоторые удивительные возможности

Теперь, если вы посмотрите на демонстрацию в Firefox, вы заметите, что консоль уже показывает несколько мутаций — даже до того, как вы нажали кнопку. Это происходит потому, что сам наблюдатель не заключен в DOMContentLoaded , поэтому он начинает работать, как только сценарий выполняется. Я обнаружил это случайно, просто потому, что я предпочитаю писать сценарии таким образом, когда это возможно, и я понял, что мутации — это браузер, добавляющий узлы в <body> — то есть по одному для каждого из узлов, которые идут после содержащего <script> .

Chrome этого не делает — и я могу только подозревать, что это намеренно предотвращено — потому что это имеет смысл в том, как мы знаем, как работают сценарии DOM . Мы знаем, что сценарии выполняются синхронно, и поэтому можно добавить в <body> до того, как он закончит рендеринг. Поэтому, если мы начинаем наблюдать изменения DOM , мы должны получать уведомления о каждом изменении, которое происходит впоследствии, даже если это изменение было вызвано собственным рендерингом браузера.

Это напоминает мне идею, которая возникла у меня пару лет назад , для библиотеки, которая обеспечивала бы обратные вызовы для нескольких различных точек во время загрузки и рендеринга документа. Я никогда не развивал эту идею, потому что это потребовало бы таких жестоких взломов — но с использованием наблюдателей мутаций это было бы тривиально и чисто. Все, что нам нужно сделать, это добавить наблюдателя в самом начале тела , а затем мы можем сидеть сложа руки и смотреть, как браузер рисует его узел за узлом!

Проверьте это (в Firefox 14 или более поздней версии):

Больше возможностей на каждый день

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

Вот еще один пример, который отслеживает изменения в тексте элемента (то есть в текстовом узле элемента firstChild ), а затем прекращает наблюдать, как только происходит изменение:

 (new MutationObserver(function(mutations, self) { mutations.forEach(function(mutation) { console.log('Changed text from "' + mutation.oldValue + '" to "' + mutation.target.nodeValue + '"'); }); self.disconnect(); })).observe(element.firstChild, { characterData : true, characterDataOldValue : true }); 

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

Вывод

Это было широкое введение в наблюдателей за мутациями, в которых довольно подробно рассказывается о том, как они используются; Я даже не упомянул тот факт, что реализация Chrome имеет префикс (пока доступен как WebKitMutationObserver ). Но я хотел сосредоточиться в основном на фоне этого нового API и начать волноваться о возможностях!

Если есть спрос, я напишу дополнительную статью, чтобы изучить их в деталях кода, но сейчас я рекомендую вам посетить документацию MutationObserver в MDN . В блоге Mozilla Hacks есть еще одна хорошая статья.

Я был довольно обескуражен, когда услышал, что мутационные события исчезают, потому что что еще может сделать ту же работу? Что ж, оказывается, что-то еще есть — и в сто раз лучше!