MutationObserver
是一个内置对象,它观察 DOM 元素并在检测到更改时触发回调函数。
我们首先看一下语法,然后探索一个实际用例,看看这种东西在什么地方有用。
语法
MutationObserver
很容易使用。
首先,我们使用回调函数创建一个观察者
let observer = new MutationObserver(callback);
然后将其附加到 DOM 节点
observer.observe(node, config);
config
是一个包含布尔选项的对象,用于指定“对哪些类型的更改做出反应”
childList
–node
的直接子节点的更改,subtree
–node
的所有后代的更改,attributes
–node
的属性,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
可以跟踪任何更改。配置“要观察的内容”选项用于优化,而不是将资源浪费在不必要的回调调用上。
评论
<code>
标签,对于多行代码,请将其包装在<pre>
标签中,对于超过 10 行的代码,请使用沙箱(plnkr,jsbin,codepen…)