DOM规范 - MutationObserver接口观察DOM元素的属性和节点变化
一、MutationObserver 接口说明
此接口可以在 DOM 元素的属性和节点被修改时异步执行回调。使用 MutationObserver 可以观察整个文档、DOM 树的一部分,或某个元素。此外还可以观察元素属性、子节点、文本,或者前三者任意组合的变化。若想监听 DOM 尺寸变化,可查看另一篇blog:ResizeObserver Api监听DOM尺寸变化
DOM3 中新引进 MutationObserver 接口是为了取代废弃的 DOM2 中的 MutationEvent。
二、基本用法
MutationObserver 的实例要通过 MutationObserver 构造函数并传入一个回调函数来创建。
let observer = new MutationObserver(() => console.log('Dom was mutated~'))
1. observe() 方法
新创建的 MutationObserver 实例不会关联 DOM 的任何部分。需要使用 ovserve()方法与 DOM 关联起来。
此方法需传两个必须的参数:
- 要观察其变化的 DOM 节点
- 一个 MutationObserverInit 对象
MutationObserverInit 对象:用于控制哪些方面的变化,是一个键/值对形式配置选项的字典。
// 创建一个观察者(observer)并配置它观察 body 元素上的属性变化
let observer = new MutationObserver(() => {
console.log('body attributes changed~')
})
observer.observe(document.body, { attributes: true })
document.body.className = 'foo'
console.log('Changed body class')
// Changed body class
// body attributes changed~
执行上述代码后,body 元素上的任何属性发生变化都会被这个 MutationObserver 实例发现,然后会异步执行注册的回调函数。而 body 元素后代的修改或其他非属性变化修改都不会触发回调进入任务队列。
注意,回调中的 console.log()是后执行的,这表明回调并非与实际的 DOM 变化同步执行。
2. 回调与 MutationRecord 参数
每个回调都会受到一个 MutationRecord 实例的数组。MutationRecord 实例包含信息包括发生了什么变化、以及 DOM 的哪一部分受到了影响。回调的第二个参数是 MutationObserver 的实例。
// 连续修改会生成多个MutationRecord实例。回调执行时会受到包含所有这些实例的数组,顺序为变化事件顺序。
let observer = new MutationObserver((MutationRecords, mutationObserver) => {
console.log(MutationRecords, mutationObserver)
})
observer.observe(document.body, { attributes: true })
document.body.setAttribute('foo', 'bar')
document.body.className = 'testName'
// [MutationRecord, MutationRecord], MutationObserver
执行效果如下图:
MutationRecord 实例属性说明
属性 | 说明 |
---|---|
target | 被修改影响的目标节点 |
type | 字符串,表示变化的类型:"attributes"、"characterData"、"childList" |
oldValue | 如果在 MutationObserverInit 对象中启用(attributeOldValue 或 characterData OldValue 为 true),"attributes" 或 "characterData" 的变化事件会设置这个属性为被替代的值"childList" 类型的变化始终将这个属性设置为 null |
attributeName | 对于 "attributes" 类型的变化,这里保存被修改属性的名字。其他变化事件会将此设置为 null |
attributeNamespace | 对于使用了命名空间的 "attributes" 类型的变化,这里保存被修改属性的名字。其他变化事件会将此设置为 null |
addedNodes | 对于 "childList" 类型的变化,返回包含变化中添加节点的 NodeList。默认为空 NodeList |
removedNodes | 对于 "childList" 类型的变化,返回包含变化中删除节点的 NodeList。默认为空 NodeList |
previousSibling | 对于 "childList" 类型的变化,返回变化节点的前一个同胞 Node。默认为 null |
nextSibling | 对于 "childList" 类型的变化,返回变化节点的后一个同胞 Node。默认为 null |
3. disconnet() 方法
默认情况下,只要被观察的元素不被垃圾回收,MutationObserver 的回调就会响应 DOM 变化事件而执行。要提前终止回调,可以调用 disconnet() 方法。
let observer = new MutationObserver(() => {
console.log('body attributes changed~')
})
observer.observe(document.body, { attributes: true })
observer.disconnet()
document.body.className = 'foo'
// (没有日志输出)
重用 MutationObserver:调用 disconnet() 方法并不会结束 MutationObserver 的生命。还可以重新使用这个观察者,再讲它关联到新的目标节点。
三、MutationObserverInit 观察范围
MutationObserverInit 对象用于控制对目标节点的观察范围。例:属性变化、文本变化和节点变化。
属性 | 说明 |
---|---|
subtree | boolean,表示除了目标节点,是否观察其子树(后代)。默认为 false,只观察目标节点的变化 |
attributes | boolean,表示是否观察目标节点的属性变化。默认为 false |
attributeFilter | 字符串数组,表示要观察哪些属性的变化。把这个值设置为 true,也会将 attributes 值转换为 true。默认为观察所有属性 |
attributeOldValue | boolean,表示 MutationRecord 是否记录变化之前的值。把这个值设置为 true,也会将 attributes 值转换为 true。默认为 false |
characterData | boolean,表示修改字符数据是否触发变化事件 |
characterOldValue | boolean,表示 MutationRecord 是否记录变化之前的值。把这个值设置为 true,也会将 characterData 值转换为 true。默认为 false |
childList | boolean,表示修改目标节点的子节点是否触发变化事件。默认为 false |
调用 observe() 时,MutationObserverInit 对象中 attributes、characterData、childList 属性必须至少有一项为 true。
<!-- 观察子节点 -->
<body>
<div id="con"></div>
<script>
const conEle = document.getElementById('con')
let observer = new MutationObserver((MutationRecords, mutationObserver) => {
console.log(MutationRecords)
})
observer.observe(conEle, { childList: true })
conEle.appendChild(document.createElement('p'))
</script>
</body>
// [MutationRecord]
打印效果如下图:
四、异步回调与记录队列
1. 记录队列
每次 MutationRecord 被添加到 MutationObserver 的记录队列时,仅当之前没有已排期的微任务回调时(队列中微任务长度为 0),才会将观察者注册的回调(在初始化 MutationObserver 时传入)作为微任务调度到任务队列上。这样可以保证记录队列的内容不会被回调处理两次。
不过在回调的微任务异步执行期间,有可能又会发生更多的变化事件。因此被处理的回调会接收到一个 MutationRecord 实例的数组,顺序为它们进入记录队列的顺序。回调要负责处理这个数组的每一个实例,因为函数退出之后这些实例就不存在了。回调执行后,这些 MutationRecord 就用不着了,因此记录队列会被清空,其内容会被丢弃。
2. takeRecords() 方法
调用 MutationObserver 实例的 takeRecords() 方法可以清空记录队列,取出并返回其中的所有 MutationRecord 实例。
这在希望断开与观察目标的联系,但有希望处理由于调用 disconnet() 而被抛弃的记录队列中的 MutationRecord 实例时比较有用。
let observer = new MutationObserver((MutationRecords) => {
console.log(MutationRecords)
})
observer.observe(document.body, { attributes: true })
document.body.className = 'foo'
document.body.className = 'bar'
console.log(111, observer.takeRecords())
console.log(222, observer.takeRecords())
// 111 [MutationRecord, MutationRecord]
// 222 []
五、内存与垃圾回收
将变化回调委托给微任务来执行可以保证事件同步触发,同时避免随之而来的混乱。为 MutationObserver 而实现的记录队列,可以保证即使变化事件被爆发式的触发,也不会显著的拖慢浏览器。
无论如何,使用 MutationObserver 仍然不是没有代价
的。因此理解什么时候避免出现这种情况就很重要了。
1. MutationObserver 的引用
MutationObserver 实例与目标节点之间的引用关系是非对称
的。MutationObserver 拥有对观察目标节点的弱引用
。因为是弱引用,所以不会妨碍垃圾回收程序回收目标节点。
然而,目标节点却拥有对 MutationObserver 的强引用
。如果目标节点从 DOM 中被移除,随后被垃圾回收,则关联的 MutationObserver 也会被垃圾回收。
2. MutationRecord 的引用
记录队列中的每个 MutationRecord 实例至少包含对已有 DOM 节点的一个引用。如果变化是 childList
类型,则会包含多个节点的引用。记录队列和回调处理的默认行为是耗尽这个队列,处理每个 MutationRecord,然后让它们超出作用域并被垃圾回收。
有时候可能需要保存某个观察者的完整变化记录。保存这些 MutationRecord 实例,也就会保存它们引用的节点,因而会妨碍这些节点的回收。如果需要尽快地释放内存,建议从每个 MutationRecord 中抽取最有用的信息,然后保存到一个新对象中,最后抛弃 MutationRecord。