了解 MutationObserver API

已发表: 2022-03-10
快速总结↬在复杂的 Web 应用程序和框架中有时需要监控 DOM 的更改。 通过解释和交互式演示,本文将向您展示如何使用 MutationObserver API 使观察 DOM 变化变得相对容易。

在复杂的 Web 应用程序中,DOM 更改可能很频繁。 因此,在某些情况下,您的应用可能需要响应对 DOM 的特定更改。

一段时间以来,寻找 DOM 更改的公认方法是通过称为 Mutation Events 的功能,该功能现已弃用。 W3C 批准的 Mutation Events 替代品是 MutationObserver API,我将在本文中详细讨论它。

许多较早的文章和参考资料讨论了为什么旧功能被替换,所以我不会在这里详细介绍(除了我无法做到公正的事实)。 MutationObserver API 具有近乎完整的浏览器支持,因此如果需要,我们可以在大多数(如果不是全部)项目中安全地使用它。

MutationObserver 的基本语法

MutationObserver可以以多种不同的方式使用,我将在本文的其余部分详细介绍,但MutationObserver的基本语法如下所示:

 let observer = new MutationObserver(callback); function callback (mutations) { // do something here } observer.observe(targetNode, observerOptions);

第一行使用MutationObserver()构造函数创建一个新的MutationObserver 。 传递给构造函数的参数是一个回调函数,将在每个符合条件的 DOM 更改时调用。

确定什么符合特定观察者的方法是通过上述代码中的最后一行。 在那条线上,我使用MutationObserverobserve()方法开始观察。 您可以将其与addEventListener()类的内容进行比较。 一旦你附加了一个监听器,页面就会“监听”指定的事件。 同样,当您开始观察时,页面将开始“观察”指定的MutationObserver

跳跃后更多! 继续往下看↓

observe()方法有两个参数: target ,应该是观察变化的节点或节点树; 和一个选项对象,它是一个MutationObserverInit对象,允许您为观察者定义配置。

MutationObserver的最后一个关键基本特性是disconnect()方法。 这允许您停止观察指定的更改,它看起来像这样:

 observer.disconnect();

配置 MutationObserver 的选项

如前所述, MutationObserverobserve()方法需要第二个参数,该参数指定描述MutationObserver的选项。 以下是包含所有可能的属性/值对的选项对象的外观:

 let options = { childList: true, attributes: true, characterData: false, subtree: false, attributeFilter: ['one', 'two'], attributeOldValue: false, characterDataOldValue: false };

在设置MutationObserver选项时,没有必要包括所有这些行。 我将这些仅用于参考目的,因此您可以查看可用的选项以及它们可以采用的值类型。 正如你所看到的,除了一个之外,所有的都是布尔值。

为了使MutationObserver工作,至少需要将childListattributescharacterData之一设置为true ,否则将引发错误。 其他四个属性与这三个属性之一结合使用(稍后会详细介绍)。

到目前为止,我只是掩盖了语法给您一个概述。 考虑这些功能如何工作的最佳方式是提供包含不同选项的代码示例和现场演示。 这就是我将在本文的其余部分做的事情。

使用 childList 观察子元素的变化

您可以启动的第一个也是最简单的MutationObserver是查找要添加或删除的指定节点(通常是元素)的子节点。 对于我的示例,我将在我的 HTML 中创建一个无序列表,并且我想知道何时从该列表元素中添加或删除子节点。

列表的 HTML 如下所示:

 <ul class="list"> <li>Apples</li> <li>Oranges</li> <li>Bananas</li> <li class="child">Peaches</li> </ul>

我的MutationObserver的 JavaScript 包括以下内容:

 let mList = document.getElementById('myList'), options = { childList: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'childList') { console.log('Mutation Detected: A child node has been added or removed.'); } } } observer.observe(mList, options);

这只是代码的一部分。 为简洁起见,我将展示处理MutationObserver API 本身的最重要部分。

请注意我是如何循环遍历mutations参数的,它是一个具有许多不同属性的MutationRecord对象。 在这种情况下,我正在读取type属性并记录一条消息,指示浏览器检测到符合条件的突变。 另外,请注意我是如何将mList元素(对我的 HTML 列表的引用)作为目标元素(即我想要观察其变化的元素)传递的。

  • 查看完整的交互式演示 →

使用按钮启动和停止MutationObserver 。 日志消息有助于澄清正在发生的事情。 代码中的注释也提供了一些解释。

请注意这里的几个要点:

  • 回调函数(我将其命名为mCallback ,以说明您可以随意命名它)将在每次检测到成功的突变时以及在执行observe()方法之后触发。
  • 在我的示例中,唯一符合条件的突变“类型”是childList ,因此在循环遍历 MutationRecord 时寻找这个是有意义的。 在这种情况下寻找任何其他类型都不会做任何事情(其他类型将在后续演示中使用)。
  • 使用childList ,我可以从目标元素中添加或删除文本节点,这也符合条件。 因此,它不必是添加或删除的元素。
  • 在此示例中,只有直接子节点才有资格。 在本文后面,我将向您展示如何将其应用于所有子节点、孙子节点等。

观察元素属性的变化

您可能想要跟踪的另一种常见类型的突变是指定元素上的属性发生更改时。 在下一个交互式演示中,我将观察段落元素属性的变化。

 let mPar = document.getElementById('myParagraph'), options = { attributes: true }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } } observer.observe(mPar, options);
  • 试用演示 →

同样,为了清楚起见,我对代码进行了缩写,但重要的部分是:

  • options对象使用attributes属性,设置为true以告诉MutationObserver我要查找对目标元素属性的更改。
  • 我在循环中测试的突变类型是attributes ,在这种情况下唯一符合条件的突变类型。
  • 我还使用了mutation对象的attributeName属性,它可以让我找出更改了哪个属性。
  • 当我触发观察者时,我通过引用传递段落元素以及选项。

在此示例中,一个按钮用于切换目标 HTML 元素上的类名。 每次添加或删除类时都会触发突变观察器中的回调函数。

观察字符数据变化

您可能希望在您的应用程序中寻找的另一个变化是字符数据的突变; 也就是说,更改特定的文本节点。 这是通过在options对象中将characterData属性设置为true来完成的。 这是代码:

 let options = { characterData: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'characterData') { // Do something here... } } }

再次注意,在回调函数中查找的typecharacterData

  • 观看现场演示 →

在此示例中,我正在寻找对特定文本节点的更改,我通过element.childNodes[0]定位该节点。 这有点hacky,但它适用于这个例子。 文本是用户可通过段落元素上的contenteditable属性进行编辑的。

观察字符数据变化时的挑战

如果您使用过contenteditable ,那么您可能会知道有允许编辑富文本的键盘快捷键。 例如,CTRL-B 使文本变为粗体,CTRL-I 使文本变为斜体,等等。 这会将文本节点分解为多个文本节点,因此您会注意到MutationObserver将停止响应,除非您编辑仍被视为原始节点一部分的文本。

我还应该指出,如果您删除所有文本, MutationObserver将不再触发回调。 我假设发生这种情况是因为一旦文本节点消失,目标元素就不再存在。 为了解决这个问题,我的演示在删除文本时停止观察,尽管当您使用富文本快捷方式时事情会变得有点棘手。

但别担心,在本文后面,我将讨论一种更好的方式来使用characterData选项,而不必处理这些怪癖。

观察指定属性的变化

早些时候,我向您展示了如何观察指定元素上属性的变化。 在这种情况下,虽然演示会触发类名更改,但我可以更改指定元素上的任何属性。 但是,如果我想观察一个或多个特定属性的变化而忽略其他属性怎么办?

我可以使用option对象中的可选attributeFilter属性来做到这一点。 这是一个例子:

 let options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'] }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }

如上所示, attributeFilter属性接受我要监视的特定属性数组。 在此示例中,每次修改hiddencontenteditabledata-par属性中的一个或多个时, MutationObserver都会触发回调。

  • 观看现场演示 →

我再次针对特定的段落元素。 请注意选择要更改的属性的下拉菜单。 draggable属性是唯一不符合条件的属性,因为我没有在选项中指定该属性。

请注意,在代码中,我再次使用MutationRecord对象的attributeName属性来记录更改了哪个属性。 当然,与其他演示一样,在单击“开始”按钮之前, MutationObserver不会开始监视更改。

我应该在这里指出的另一件事是,在这种情况下,我不需要将attributes值设置为true 。 由于attributesFilter被设置为 true,这是隐含的。 这就是为什么我的选项对象可能如下所示,并且它的工作方式相同:

 let options = { attributeFilter: ['hidden', 'contenteditable', 'data-par'] }

另一方面,如果我将attributesattributeFilter数组一起显式设置为false ,它将不起作用,因为false值将优先,并且过滤器选项将被忽略。

观察节点及其子树的变化

到目前为止,在设置每个MutationObserver时,我只处理目标元素本身,在childList的情况下,是元素的直接子元素。 但肯定有一种情况,我可能想观察以下其中一项的变化:

  • 一个元素及其所有子元素;
  • 一个元素及其子元素的一个或多个属性;
  • 元素内的所有文本节点。

以上所有都可以使用选项对象的subtree属性来实现。

childList 带子树

首先,让我们看看元素子节点的变化,即使它们不是直接子节点。 我可以将我的选项对象更改为如下所示:

 options = { childList: true, subtree: true }

代码中的其他所有内容都或多或少与前面的childList示例相同,还有一些额外的标记和按钮。

  • 观看现场演示 →

这里有两个列表,一个嵌套在另一个列表中。 当MutationObserver启动时,回调将触发对任一列表的更改。 但是如果我将subtree属性改回false (不存在时的默认值),则在修改嵌套列表时回调将不会执行。

带有子树的属性

这是另一个示例,这次使用带有attributesattributeFiltersubtree 。 这使我不仅可以观察目标元素的属性更改,还可以观察目标元素的任何子元素的属性的更改:

 options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'], subtree: true }
  • 观看现场演示 →

这与之前的属性演示类似,但这次我设置了两个不同的选择元素。 第一个修改目标段落元素的属性,而另一个修改段落内子元素的属性。

同样,如果您将subtree选项设置回false (或删除它),第二个切换按钮将不会触发MutationObserver回调。 而且,当然,我可以完全省略attributeFilter ,而MutationObserver会查找对子树中任何属性的更改,而不是指定的属性。

characterData 带子树

请记住,在之前的characterData演示中,目标节点消失以及MutationObserver不再工作时存在一些问题。 虽然有一些方法可以解决这个问题,但直接定位元素而不是文本节点更容易,然后使用subtree属性来指定我希望该元素内的所有字符数据(无论它嵌套多深)触发MutationObserver回调。

在这种情况下,我的选择如下所示:

 options = { characterData: true, subtree: true }
  • 观看现场演示 →

启动观察者后,尝试使用 CTRL-B 和 CTRL-I 来设置可编辑文本的格式。 您会注意到这比前面的characterData示例更有效。 在这种情况下,分解的子节点不会影响观察者,因为我们观察的是目标节点内的所有节点,而不是单个文本节点。

记录旧值

通常,在观察 DOM 的变化时,您会想要记下旧值并可能将它们存储或在其他地方使用它们。 这可以使用options对象中的几个不同属性来完成。

属性旧值

首先,让我们尝试在旧属性值更改后注销它。 以下是我的选项与回调的外观:

 options = { attributes: true, attributeOldValue: true } function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
  • 观看现场演示 →

注意MutationRecord对象的attributeNameoldValue属性的使用。 通过在文本字段中输入不同的值来尝试演示。 请注意日志如何更新以反映之前存储的值。

字符数据旧值

同样,如果我想记录旧字符数据,我的选项如下所示:

 options = { characterData: true, subtree: true, characterDataOldValue: true }
  • 观看现场演示 →

请注意日志消息指示先前的值。 当您通过富文本命令将 HTML 添加到组合中时,事情确实会变得有些不稳定。 我不确定在这种情况下正确的行为应该是什么,但如果元素内唯一的东西是单个文本节点,它会更直接。

使用 takeRecords() 拦截突变

我还没有提到的MutationObserver对象的另一种方法是takeRecords() 。 此方法允许您或多或少地拦截在回调函数处理之前检测到的突变。

我可以使用这样的行来使用此功能:

 let myRecords = observer.takeRecords();

这将 DOM 更改的列表存储在指定变量中。 在我的演示中,只要单击修改 DOM 的按钮,我就会执行此命令。 请注意,开始和添加/删除按钮不记录任何内容。 这是因为,如前所述,我在回调处理它们之前拦截了 DOM 更改。

但是请注意,我在停止观察者的事件侦听器中所做的事情:

 btnStop.addEventListener('click', function () { observer.disconnect(); if (myRecords) { console.log(`${myRecords[0].target} was changed using the ${myRecords[0].type} option.`); } }, false);

如您所见,在使用observer.disconnect()停止观察者后,我正在访问被拦截的突变记录,并且正在记录目标元素以及记录的突变类型。 如果我一直在观察多种类型的更改,那么存储的记录中将包含多个项目,每个项目都有自己的类型。

当通过调用takeRecords()以这种方式截获突变记录时,通常会发送到回调函数的突变队列被清空。 因此,如果由于某种原因您需要在处理这些记录之前拦截它们, takeRecords()会派上用场。

使用单个观察者观察多个变化

请注意,如果我在页面上的两个不同节点上寻找突变,我可以使用同一个观察者来完成。 这意味着在我调用构造函数之后,我可以对任意数量的元素执行observe()方法。

因此,在这一行之后:

 observer = new MutationObserver(mCallback);

然后,我可以使用不同的元素作为第一个参数进行多次observe()调用:

 observer.observe(mList, options); observer.observe(mList2, options);
  • 观看现场演示 →

启动观察者,然后尝试两个列表的添加/删除按钮。 这里唯一的问题是,如果您点击“停止”按钮之一,观察者将停止观察两个列表,而不仅仅是它所针对的列表。

移动正在观察的节点树

我要指出的最后一件事是MutationObserver将继续观察对指定节点的更改,即使该节点已从其父元素中删除。

例如,试试下面的演示:

  • 观看现场演示 →

这是另一个使用childList监视目标元素的子元素更改的示例。 注意断开子列表的按钮,这是被观察的。 单击“开始...”按钮,然后单击“移动...”按钮移动嵌套列表。 即使在列表从其父级中删除后, MutationObserver仍会继续观察指定的更改。 发生这种情况并不令人意外,但需要牢记这一点。

结论

这几乎涵盖了MutationObserver API 的所有主要功能。 我希望本次深入探讨对您熟悉此标准有所帮助。 如前所述,浏览器支持非常强大,您可以在 MDN 页面上阅读有关此 API 的更多信息。

我已将本文的所有演示放入 CodePen 集合中,如果您想有一个简单的地方来摆弄演示。