Если вы веб-разработчик, вы, вероятно, знакомы с концепцией манипулирования DOM. При динамическом обновлении HTML, CSS или структуры объектной модели документа (DOM) может быть сложно отслеживать изменения в структуре или содержимом элемента. Здесь в игру вступает Mutation Observer.

Mutation Observer — это API JavaScript, который позволяет обнаруживать изменения в DOM и реагировать на них. С помощью Mutation Observer вы можете отслеживать изменения в определенных узлах, отслеживать определенные атрибуты или дочерние элементы и выполнять пользовательские функции в ответ на эти изменения.

Некоторые практические варианты использования этого API могут быть

  1. Обновление пользовательского интерфейса в ответ на действия пользователя: Mutation Observer можно использовать для обновления пользовательского интерфейса в режиме реального времени в ответ на действия пользователя. Например, вы можете использовать Mutation Observer для обновления пользовательского интерфейса, когда пользователь добавляет или удаляет товары из корзины.
  2. Отложенная загрузка изображений. Mutation Observer также можно использовать для реализации отложенной загрузки изображений. Наблюдая за изменениями в DOM, вы можете определить, когда изображение становится видимым, а затем загрузить изображение в этот момент. Это может повысить производительность вашего веб-сайта за счет уменьшения объема данных, которые необходимо загрузить при начальной загрузке страницы.
  3. Проверка форм. Mutation Observer можно использовать для проверки форм в режиме реального времени. Наблюдая за изменениями в форме, вы можете определить, когда пользователь ввел неверные данные, и предоставить обратную связь в режиме реального времени. Это может улучшить взаимодействие с пользователем и снизить вероятность ошибок пользователя.

Есть также видеоверсия этого видео на Youtube.

Создание наблюдателя мутаций

Вы создаете новый MutationObserver, используя его constructor, и передаете в него функцию обратного вызова. Этот наблюдатель предоставляет метод observe, который принимает 2 аргумента. Первый — это узел или дерево узлов, для которых вы хотите наблюдать за изменениями. Второй аргумент — это объект опций, который позволяет вам определить конфигурацию наблюдателя.

function cb(mutations){    
  //do something on mutation 
}

const observer = new MutationObserver(cb)

observer.observe(target_node, options)

Параметры конфигурации снова довольно просты.

  • childList: Boolean — если установлено значение true, отслеживает добавление или удаление дочерних узлов внутри целевого/родительского узла.
  • attributes: Boolean —отслеживает изменения значений атрибутов на узле.
  • characterData: Boolean — отслеживает изменения в символьных данных, содержащихся в узле.

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

  • subtree: Boolean — первые три свойства применимы к узлу, который мы передаем внутри метода observe. Если мы добавим свойство subtree, то оно будет отслеживать все элементы в целевом узле (вложенные элементы) на наличие мутаций. Так, например, установка свойства childList в значение true будет отслеживать мутации для прямых дочерних элементов целевого узла. Установка subtree в значение true вместе со свойством childList теперь будет отслеживать все вложенные элементы, а не только узлы верхнего уровня. Вы также можете соединить subtree с attributes или characterData.
  • attributeFilter: Array — это единственное свойство, которое ожидает массив вместо логического значения. Это свойство указывает, какие атрибуты следует отслеживать, и игнорирует остальные для повышения производительности.
  • attributeOldValue: логическое значение — используется вместе с attributes. Будет записывать предыдущее значение атрибута при каждой мутации.
  • characterDataOldValue: логическое значение — используется вместе с characterData. Будет записывать предыдущее значение символьных данных текстового узла при каждой мутации.

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

observer.disconnect()

Наблюдение за childList

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

<ul id="item_list">
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
    <li>Item 4</li>
</ul>
<button id="add_item">Add item</button>
<button id="remove_item">Remove item</button>

<script>
    const addButton = document.querySelector("#add_item")
    const removeButton = document.querySelector("#remove_item")
    const list = document.querySelector("#item_list")

    addButton.addEventListener("click", (e) => {
        const currentId = list.children.length
        const listItem = document.createElement('li')
        listItem.innerHTML = `<li>Item ${currentId + 1}</li>`
        list.append(listItem)
    })
    removeButton.addEventListener("click", (e) => {
        const lastElementIndex = list.children.length - 1;
        const lastElement = list.children[lastElementIndex]
        list.removeChild(lastElement)
    })
</script>

Теперь создайте наблюдателя мутаций и добавьте его прямо под слушателями событий. Внутри функции обратного вызова я просто выведу массив мутаций в консоль. Массив мутаций, который мы получаем как часть обратного вызова, действует как очередь для всех мутаций, происходящих внутри дерева узлов. Я также установил для свойства childList значение true, поэтому наблюдатель будет сообщать только о мутациях, которые затрагивают непосредственные дочерние элементы списка, то есть элементы <li>.

...
const callbackFunc = (mutations) => {
    console.log(mutations);
}

const observer = new MutationObserver(callbackFunc)
observer.observe(list, {
    childList: true
})

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

//ChildList Mutation Record
{
  addedNodes: NodeList [li]
  attributeName: null
  attributeNamespace: null
  nextSibling: null
  oldValue: null
  previousSibling: li
  removedNodes: NodeList []
  target: ul#list
  type: "childList"
}

Это дает вам хорошее представление о мутации, ее типе, из какого узла она возникла и т. Д. P.S. Если вы удалите элемент вместо добавления, свойство removedNodes будет иметь элемент внутри списка.

Наблюдение за атрибутами

Второй элемент данных, который мы отслеживаем с помощью Mutation Observers, будет attributes. Чтобы увидеть это в действии, давайте добавим атрибут стиля к элементу списка и установим для фона определенный цвет. Кроме того, не забудьте установить для свойства attribute значение true внутри объекта конфигурации наблюдателя.

<ul id="item_list" style="background: yellow;">
  ...
</ul>


...
observer.observe(list, {
    childList: true,
    attributes: true,
})

Внутри браузера, в разделе элементов инструментов разработчика, попробуйте изменить цвет фона на что-то другое. В консоли вы найдете запись мутации типа attributes.

//Attributes Mutation Record
{
  addedNodes: NodeList []
  attributeName: "style"
  attributeNamespace: null
  nextSibling: null
  oldValue: null   <--- Can get this value by adding attributeOldValue
  previousSibling: null
  removedNodes: NodeList []
  target: ul#list
  type: "attributes"
}

Если вы добавите свойство attributeOldValue в конфигурацию наблюдателя и установите для него значение true, вы также получите предыдущее значение для мутации.

Наконец, когда дело доходит до события attributes, у вас также есть возможность выборочно выбирать, какие атрибуты отслеживать на наличие изменений, используя параметр attributeFilter. В нашем текущем примере мы отслеживаем только один атрибут, но это будет иметь больше смысла, если у вас есть элемент с кучей атрибутов.

Попробуйте передать пустой массив в свойство attributeFilter внутри конфигурации наблюдателя. Это явно указывает наблюдателю игнорировать все атрибуты, поэтому теперь, когда вы редактируете цвет фона списка, в консоли не должно быть записей об изменениях.

Наблюдение за характерными данными

Теперь окончательный элемент данных, который является characterData, немного сложен. Сначала нам нужно понять, что такое characterData. Любой текстовый элемент, который вы знаете, например тег <p>, тег <span> или даже тег <li>, имеет внутри себя текстовый узел. Данные, которые присутствуют внутри этого текстового узла, называются символьными данными.

Таким образом, внутри вашей консоли вы можете получить доступ к дочерним узлам списка, используя list.childNodes. Внутри списка вы найдете элементы <li> вместе с некоторыми текстовыми узлами. Эти текстовые узлы представляют новую строку строки перед каждым элементом списка. Таким образом, хотя вы думаете, что в списке всего 4 узла, на самом деле их 8.

Теперь откройте любой из элементов списка, и вы найдете еще одно свойство childNodes с одним текстовым узлом в нем. Это текстовый узел, содержащий символьные данные элемента <li>.

Теперь, чтобы проверить это в нашем примере, мы внесем небольшие изменения. Мы проверим это на первом <li> элементе списка. Поэтому добавьте атрибут contenteditable к этому элементу, чтобы мы могли изменить его содержимое/символьные данные. Кроме того, добавьте параметр characterData и установите для него значение true внутри конфигурации.

<ul id="list" style="background: yellow;">
  <li contenteditable="true">Item 1</li>
  ...
</ul>


...
observer.observe(list, {
    childList: true,
    attributes: true,
    attributeOldValue: true,
    characterData: true
})

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

...
const list = document.querySelector("#list")
const firstItem = list.childNodes[1].childNodes[0]
...


observer.observe(firstItem, {
  ...
})

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

//CharacterData Mutation Record
{
  addedNodes: NodeList []
  attributeName: null
  attributeNamespace: null
  nextSibling: null
  oldValue: null   <--- Can get this value by adding characterDataOldValue
  previousSibling: null
  removedNodes: NodeList []
  target: text
  type: "characterData"
}

Теперь цель должна указывать на текстовый узел. Аналогично attributeOldValue в случае типа мутации attributes вы можете прикрепить characterDataOldValue к объекту конфигурации, чтобы получить предыдущее значение.

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

Чтобы решить эту проблему, мы можем использовать свойство subtree. Если для свойства subtree установлено значение true, все элементы внутри тега <li> будут отслеживаться на предмет изменений. Таким образом, даже после того, как вы избавитесь от текстового узла, новый текстовый узел, который вы получите, когда снова начнете печатать, все равно будет отслеживаться. Таким образом, вы увидите записи о мутациях даже после удаления и повторного ввода текста.

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

Теперь внутри скрипта вам не нужно отслеживать элемент списка. Поскольку вы будете использовать свойство subtree, все внутри элемента списка будет отслеживаться. Также не забудьте установить для свойства subtree значение true внутри конфигурации.

const firstItem = list.childNodes[1]  //This will work just fine
...
observer.observe(firstItem, {
  ...
  subtree: true
})

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

Заключение

Это должно сделать это для этого поста. Mutation Observer — это мощный JavaScript API, который позволяет обнаруживать изменения в DOM и реагировать на них. Используя Mutation Observer, вы можете создавать динамические и отзывчивые веб-приложения, улучшающие взаимодействие с пользователем. Итак, являетесь ли вы новичком или опытным разработчиком, обязательно добавьте Mutation Observer в свой набор инструментов и начните изучать его многочисленные возможности.

У меня также есть видеоверсия этого поста на Youtube для тех, кто лучше разбирается в видеоконтенте.

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

YouTube
LinkedIn
Twitter
GitHub

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .

Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.