2020年9月13日

MutationObserver

MutationObserver 是一个内置对象,它观察 DOM 元素并在检测到更改时触发回调函数。

我们首先看一下语法,然后探索一个实际用例,看看这种东西在什么地方有用。

语法

MutationObserver 很容易使用。

首先,我们使用回调函数创建一个观察者

let observer = new MutationObserver(callback);

然后将其附加到 DOM 节点

observer.observe(node, config);

config 是一个包含布尔选项的对象,用于指定“对哪些类型的更改做出反应”

  • childListnode 的直接子节点的更改,
  • subtreenode 的所有后代的更改,
  • attributesnode 的属性,
  • attributeFilter – 属性名称数组,用于仅观察选定的属性。
  • characterData – 是否观察 node.data(文本内容),

其他一些选项

  • attributeOldValue – 如果为 true,则将属性的旧值和新值都传递给回调(见下文),否则仅传递新值(需要 attributes 选项),
  • characterDataOldValue – 如果为 true,则将 node.data 的旧值和新值都传递给回调(见下文),否则仅传递新值(需要 characterData 选项)。

然后,在任何更改之后,都会执行 callback:更改作为 MutationRecord 对象列表传递给第一个参数,观察者本身作为第二个参数传递。

MutationRecord 对象具有以下属性

  • type – 变异类型,以下之一
    • "attributes": 属性已修改
    • "characterData": 数据已修改,用于文本节点,
    • "childList": 子元素已添加/删除,
  • target – 更改发生的位置:对于 "attributes" 来说是元素,对于 "characterData" 来说是文本节点,对于 "childList" 变异来说是元素,
  • addedNodes/removedNodes – 已添加/删除的节点,
  • previousSibling/nextSibling – 已添加/删除节点的先前和下一个兄弟节点,
  • attributeName/attributeNamespace – 已更改属性的名称/命名空间(对于 XML),
  • oldValue – 上一个值,仅用于属性或文本更改,如果相应的选项设置为 attributeOldValue/characterDataOldValue

例如,这里有一个带有 contentEditable 属性的 <div>。该属性允许我们聚焦它并进行编辑。

<div contentEditable id="elem">Click and <b>edit</b>, please</div>

<script>
let observer = new MutationObserver(mutationRecords => {
  console.log(mutationRecords); // console.log(the changes)
});

// observe everything except attributes
observer.observe(elem, {
  childList: true, // observe direct children
  subtree: true, // and lower descendants too
  characterDataOldValue: true // pass old data to callback
});
</script>

如果我们在浏览器中运行这段代码,然后聚焦给定的 <div> 并更改 <b>edit</b> 内部的文本,console.log 将显示一个变异

mutationRecords = [{
  type: "characterData",
  oldValue: "edit",
  target: <text node>,
  // other properties empty
}];

如果我们进行更复杂的编辑操作,例如删除 <b>edit</b>,变异事件可能包含多个变异记录

mutationRecords = [{
  type: "childList",
  target: <div#elem>,
  removedNodes: [<b>],
  nextSibling: <text node>,
  previousSibling: <text node>
  // other properties empty
}, {
  type: "characterData"
  target: <text node>
  // ...mutation details depend on how the browser handles such removal
  // it may coalesce two adjacent text nodes "edit " and ", please" into one node
  // or it may leave them separate text nodes
}];

因此,MutationObserver 允许对 DOM 子树中的任何更改做出反应。

集成用法

什么时候有用呢?

想象一下,你需要添加一个包含有用功能的第三方脚本,但它也做了一些你不想要的事情,例如显示广告 <div class="ads">不想要的广告</div>

当然,第三方脚本没有提供删除它的机制。

使用 MutationObserver,我们可以检测到不想要的元素何时出现在我们的 DOM 中,并将其删除。

还有一些其他情况,第三方脚本会向我们的文档中添加一些内容,我们希望在它发生时检测到,以便调整我们的页面,动态调整某些内容的大小等等。

MutationObserver 允许实现这一点。

架构用法

还有一些情况,从架构的角度来看,MutationObserver 很好。

假设我们正在制作一个关于编程的网站。自然地,文章和其他材料可能包含源代码片段。

HTML 标记中的此类片段如下所示

...
<pre class="language-javascript"><code>
  // here's the code
  let hello = "world";
</code></pre>
...

为了更好的可读性,同时为了美化它,我们将在我们的网站上使用一个 JavaScript 语法高亮库,比如 Prism.js。为了获得 Prism 中上述片段的语法高亮,调用 Prism.highlightElem(pre),它会检查此类 pre 元素的内容,并向这些元素添加用于彩色语法高亮的特殊标签和样式,类似于你在本页示例中看到的内容。

我们应该在什么时候运行高亮方法呢?好吧,我们可以在 DOMContentLoaded 事件中执行它,或者将脚本放在页面底部。当我们的 DOM 准备就绪时,我们可以搜索 pre[class*="language"] 元素,并在其上调用 Prism.highlightElem

// highlight all code snippets on the page
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);

到目前为止一切都很简单,对吧?我们在 HTML 中找到代码片段并突出显示它们。

现在让我们继续。假设我们要从服务器动态获取材料。我们将在 本教程的后面 学习相关方法。现在,我们只需要知道我们从 Web 服务器获取 HTML 文章并按需显示它。

let article = /* fetch new content from server */
articleElem.innerHTML = article;

新的 article HTML 可能包含代码片段。我们需要在它们上调用 Prism.highlightElem,否则它们将不会被高亮显示。

在哪里以及何时为动态加载的文章调用 Prism.highlightElem 呢?

我们可以将该调用附加到加载文章的代码中,如下所示

let article = /* fetch new content from server */
articleElem.innerHTML = article;

let snippets = articleElem.querySelectorAll('pre[class*="language-"]');
snippets.forEach(Prism.highlightElem);

…但是,想象一下,如果我们在代码中有很多地方加载我们的内容——文章、测验、论坛帖子等等。我们需要在每个地方都放置高亮调用,以便在加载后高亮显示内容中的代码吗?这很不方便。

如果内容是由第三方模块加载的呢?例如,我们有一个由其他人编写的论坛,它动态加载内容,我们想为它添加语法高亮。没有人喜欢修补第三方脚本。

幸运的是,还有另一种选择。

我们可以使用 MutationObserver 自动检测代码片段何时插入页面并突出显示它们。

因此,我们将集中在一个地方处理突出显示功能,从而免去我们集成它的麻烦。

动态突出显示演示

这是一个工作示例。

如果您运行此代码,它将开始观察下面的元素,并突出显示出现在那里的任何代码片段。

let observer = new MutationObserver(mutations => {

  for(let mutation of mutations) {
    // examine new nodes, is there anything to highlight?

    for(let node of mutation.addedNodes) {
      // we track only elements, skip other nodes (e.g. text nodes)
      if (!(node instanceof HTMLElement)) continue;

      // check the inserted element for being a code snippet
      if (node.matches('pre[class*="language-"]')) {
        Prism.highlightElement(node);
      }

      // or maybe there's a code snippet somewhere in its subtree?
      for(let elem of node.querySelectorAll('pre[class*="language-"]')) {
        Prism.highlightElement(elem);
      }
    }
  }

});

let demoElem = document.getElementById('highlight-demo');

observer.observe(demoElem, {childList: true, subtree: true});

这里,下面,有一个 HTML 元素和 JavaScript,它使用 innerHTML 动态填充它。

请运行前面的代码(上面,观察该元素),然后运行下面的代码。您将看到 MutationObserver 如何检测和突出显示代码片段。

一个带有 id="highlight-demo" 的演示元素,运行上面的代码来观察它。

以下代码填充其 innerHTML,这会导致 MutationObserver 反应并突出显示其内容。

let demoElem = document.getElementById('highlight-demo');

// dynamically insert content with code snippets
demoElem.innerHTML = `A code snippet is below:
  <pre class="language-javascript"><code> let hello = "world!"; </code></pre>
  <div>Another one:</div>
  <div>
    <pre class="language-css"><code>.class { margin: 5px; } </code></pre>
  </div>
`;

现在我们有了 MutationObserver,它可以跟踪观察到的元素或整个 document 中的所有突出显示。我们可以在 HTML 中添加/删除代码片段,而无需考虑它。

其他方法

有一种方法可以停止观察节点。

  • observer.disconnect() – 停止观察。

当我们停止观察时,可能某些更改尚未由观察者处理。在这种情况下,我们使用

  • observer.takeRecords() – 获取未处理的变异记录列表——那些已经发生但回调尚未处理的记录。

这些方法可以一起使用,如下所示

// get a list of unprocessed mutations
// should be called before disconnecting,
// if you care about possibly unhandled recent mutations
let mutationRecords = observer.takeRecords();

// stop tracking changes
observer.disconnect();
...
observer.takeRecords() 返回的记录将从处理队列中删除。

回调不会针对由 observer.takeRecords() 返回的记录调用。

垃圾回收交互

观察者在内部使用对节点的弱引用。也就是说,如果一个节点从 DOM 中删除,并且变得不可达,那么它就可以被垃圾回收。

仅仅因为一个 DOM 节点被观察并不意味着它不会被垃圾回收。

总结

MutationObserver 可以对 DOM 中的更改做出反应——属性、文本内容以及添加/删除元素。

我们可以使用它来跟踪由我们代码的其他部分引入的更改,以及与第三方脚本集成。

MutationObserver 可以跟踪任何更改。配置“要观察的内容”选项用于优化,而不是将资源浪费在不必要的回调调用上。

教程地图

评论

在评论之前请阅读…
  • 如果您有改进建议,请提交 GitHub 问题或拉取请求,而不是评论。
  • 如果您不理解文章中的某些内容,请详细说明。
  • 要插入少量代码,请使用<code>标签,对于多行代码,请将其包装在<pre>标签中,对于超过 10 行的代码,请使用沙箱(plnkrjsbincodepen…)