DOM变动观察器
“你只见到这么多人说他不好时, 却看到他有出来说別人一句吗?”
MutationObserver 是一个内建对象,它观察 DOM 元素,在其发生更改时触发回调。
我们将首先看一下语法,然后探究一个实际的用例,以了解它在什么地方有用。
语法
MutationObserver 使用简单。
首先,我们创建一个带有回调函数的观察器:
1 | let observer = new MutationObserver(callback); |
然后将其附加到一个 DOM 节点:
1 | observer.observe(node, config); |
config 是一个具有布尔选项的对象,该布尔选项表示“将对哪些更改做出反应”:
- childList —— node 的直接子节点的更改,
- subtree —— node 的所有后代的更改,
- attributes —— node 的特性(attribute),
- attributeFilter —— 特性名称数组,只观察选定的特性。
- characterData —— 是否观察 node.data(文本内容),
其他几个选项:
- attributeOldValue —— 如果为 true,则将特性的旧值和新值都传递给回调(参见下文),否则只传新值(需要 attributes 选项),
- characterDataOldValue —— 如果为 true,则将 node.data 的旧值和新值都传递给回调(参见下文),否则只传新值(需要 characterData 选项)。
然后,在发生任何更改后,将执行“回调”:更改被作为一个 MutationRecord 对象列表传入第一个参数,而观察器自身作为第二个参数。
MutationRecord 对象具有以下属性:
type —— 变动类型,以下类型之一:
“attributes”:特性被修改了,
“characterData”:数据被修改了,用于文本节点,
“childList”:添加/删除了子元素。
target —— 更改发生在何处:”attributes” 所在的元素,或 “characterData” 所在的文本节点,或 “childList” 变动所在的元素,
addedNodes/removedNodes —— 添加/删除的节点,
previousSibling/nextSibling —— 添加/删除的节点的上一个/下一个兄弟节点,
attributeName/attributeNamespace —— 被更改的特性的名称/命名空间(用于 XML),
oldValue —— 之前的值,仅适用于特性或文本更改,如果设置了相应选项 attributeOldValue/characterDataOldValue。
例如,这里有一个
contentEditable
特性。该特性使我们可以聚焦和编辑元素。
1 | <div contentEditable id="elem">Click and <b>edit</b>, please</div> |
如果我们在浏览器中运行上面这段代码,并聚焦到给定的 <div>
上,然后更改 <b>edit</b>
中的文本,console.log
将显示一个变动:
1 | mutationRecords = [{ |
如果我们进行更复杂的编辑操作,例如删除 edit,那么变动事件可能会包含多个变动记录:
1 | mutationRecords = [{ |
因此,MutationObserver
允许对 DOM 子树中的任何更改作出反应。
用于集成
在什么时候可能有用?
想象一下,你需要添加一个第三方脚本,该脚本不仅包含有用的功能,还会执行一些我们不想要的操作,例如显示广告 <div class="ads">Unwanted ads</div>
。
当然,第三方脚本没有提供删除它的机制。
使用 MutationObserver,我们可以监测到我们不需要的元素何时出现在我们的 DOM 中,并将其删除。
还有一些其他情况,例如第三方脚本会将某些内容添加到我们的文档中,并且我们希望检测出这种情况何时发生,以调整页面,动态调整某些内容的大小等。
MutationObserver 使我们能够实现这种需求。
用于架构
从架构的角度来看,在某些情况下,MutationObserver 有不错的作用。
假设我们正在建立一个有关编程的网站。自然地,文章和其他材料中可能包含源代码段。
在HTML 标记(markup)中的此类片段如下所示:
1 | ... |
另外,我们还将在网站上使用 JavaScript 高亮显示库,例如 Prism.js。调用 Prism.highlightElem(pre)
会检查此类 pre 元素的内容,并在其中添加特殊标签(tag)和样式,以进行彩色语法高亮显示,类似于你在本文的示例中看到的那样。
那什么时候运行该高亮显示方法呢?我们可以在 DOMContentLoaded 事件中或者在页面尾部运行。到那时,我们的 DOM 已准备就绪,我们可以搜索元素 pre[class*="language"]
并对其调用 Prism.highlightElem:
1 | // 高亮显示页面上的所有代码段 |
到目前为止,一切都很简单,对吧?HTML 中有 <pre>
代码段,我们高亮显示它们。
现在让我们继续。假设我们要从服务器动态获取资料。我们将 在本教程的后续章节 中学习进行此操作的方法。目前,只需要关心我们从网络服务器获取 HTML 文章并按需显示
1 | let article = /* 从服务器获取新内容 */ |
新的 article HTML 可能包含代码段。我们需要对其调用 Prism.highlightElem,否则它们将不会被高亮显示。
对于动态加载的文章,应该在何处何时调用 Prism.highlightElem?
我们可以将该调用附加到加载文章的代码中,如下所示:
1 | let article = /* 从服务器获取新内容 */ |
……但是,想象一下,代码中有很多地方可以加载内容:文章,测验,论坛帖子。我们是否需要在每个地方都附加一个高亮显示调用?那不太方便,也很容易忘记。
并且,如果内容是由第三方模块加载的,该怎么办?例如,我们有一个由其他人编写的论坛,该论坛可以动态加载内容,并且我们想为其添加语法高亮显示。没有人喜欢修补第三方脚本。
幸运的是,还有另一种选择。
我们可以使用 MutationObserver 来自动检测何时在页面中插入了代码段,并高亮显示之它们。
因此,我们在一个地方处理高亮显示功能,从而使我们无需集成它。
1 | let observer = new MutationObserver(mutations => { |
下面有一个 HTML 元素,以及使用 innerHTML 动态填充它的 JavaScript。
请先运行前面那段代码(上面那段,观察元素),然后运行下面这段代码。你将看到 MutationObserver 是如何检测并高亮显示代码段的。
一个具有 id=”highlight-demo” 的示例元素,运行上面那段代码来观察它。
下面这段代码填充了其 innerHTML,这导致 MutationObserver 作出反应,并突出显示其内容:
1 | let demoElem = document.getElementById('highlight-demo'); |
现在我们有了 MutationObserver,它可以跟踪观察到的元素中的,或者整个 document 中的所有高亮显示。我们可以在 HTML 中添加/删除代码段,而无需考虑高亮问题。