DOM 规范 —— MutationObserver 接口
最近在重学 JavaScript
中,再一次接触到了 MutationObserver
内容,接着联想到了 Vue
源码中有使用过这个接口,因此觉得有必要对 MutationObserver
接口进行相关了解和学习。
下面是 vue
源码中关于 MutationObserver
接口使用的代码:
MutationObserver
主要作用
MutationObserver 可以观察整个 文档、DOM 树的一部分 或 具体 dom 元素,主要是观察元素的 属性、子节点、文本 的变化,并且可以在 DOM 被修改时异步执行回调。
MutationObserver 接口是为了取代废弃的 MutationEvent:
DOM Level 2 规范中描述的 MutationEvent 定义了一组会在各种 DOM 变化时触发的事件。由于浏览器事件的实现机制,这个接口出现了严重的性能问题。因此,DOM Level 3 规定废弃了这些事件。 - MutationObserver 接口更实用、性能更好
基本用法
MutationObserver 的实例要通过调用 MutationObserver 构造函数并传入一个回调函数来创建,这个回调函数会接收两个参数:
mutationRecord
—— 是一个数组存储的是 MutationRecord 的实例,数组的每一项包含发生了什么变化,以及 DOM 的哪一部分受到了影响。因为回调执行之前可能同时发生多个满足被观察 dom 修改的条件,所以当前回调就会被执行多次,每次执行回调都会传入一个包含按顺序入队的 MutationRecord 实例的数组;mutationObserver
—— 是观察变化的 MutationObserver 实例,也就是外部实例化得到的 observer 对象;
let observer = new MutationObserver((mutationRecord, mutationObserver) => { console.log('DOM was mutated!'); }); console.log("observer = ", observer); 复制代码
并且得到的 observe
实例可以调用 MutationObserver
原型上的三个方法:
observe()
disconnect()
takeRecords()
MutationObserverInit 对象
在正式介绍以上三个方法之前,有必要先了解一下 MutationObserverInit 对象,因为 observe() 方法的第二个参数需要接收的就是一个 MutationObserverInit 对象。
MutationObserverInit 对象用于控制对目标节点的观察范围,简单点说,就是 observe 实例可以检测的事件内容包括:
属性变化 —— 如:dom.removeAttribute() || dom.setAttribute() 等
文本变化 —— 如: dom.innerText = xxx || dom.innerHTML = xxx || dom.textContent = xxx 等
子节点变化 —— 如:dom.appendChild() || dom.insertBefore() || dom.replaceChild() || dom.removeChild() 等
MutationObserverInit 对象的属性,它们的值除了 attributeOldValue 属性值为数组之外,全为 Boolean 类型:
subtree —— true 表示需要检测子节点的变化,false 则相反
attributes —— true 表示需要检测属性变化,false 则相反
attributeFilter —— 字符串数组,表示要观察哪些属性的变化
attributeOldValue —— true 表示 MutationRecord 需要记录变化之前的属性值,false 则相反,一旦这个属性设置为 true ,会把 attributes 的值也设置为 true
characterData —— true 表示修改文本内容是否触发变化事件,false 则相反
characterDataOldValue —— true 表示 MutationRecord 需要记录变化之前的字符数据,false 则相反,一旦这个属性设置为 true ,会把 characterData 的值也设置为 true
childList —— 表示修改目标节点的子节点是否触发变化事件,false 则相反
总结,就是一个对象拥有符合 MutationObserverInit 上定义的这些属性,就能被称为 MutationObserverInit 对象
在调用 observe() 时,MutationObserverInit 对象中的 【attribute、characterData 、childList】或 a【ttributeOldValue、characterDataOldValue】 必须至少有一项为 true。否则会抛出错误,因为没有任何变化事件能触发回调,但是又注册了回调。
observe() 方法
关联 observer 和 dom
新创建的 MutationObserver 实例不会关联 DOM 的任何部分,必须要通过 observer.observe() 方法,把 observer 与 DOM 进行关联。
observer.observe(dom, mutationObserverInit) 中两个必需参数:
dom
—— 要观察其变化的 DOM 节点mutationObserverInit
—— 符合 MutationObserverInit 定义的对象
下面的例子就是观察 <body> 标签上的属性变化:
// 实例化 observer 并注册回调 let observer = new MutationObserver((mutationRecord, mutationObserver) =>{ // 大约 2s 执行这个回调 console.log('body attributes changed!!!'); // body attributes changed!!! console.log('mutationRecord = ', mutationRecord); // [MutationRecord] console.log('mutationObserver === observer', mutationObserver === observer);// true }); // 将 observer 实例与目标 dom 进行关联 observer.observe(document.body, { attributes: true }); // 大约 2s 后修改 body 标签的 class 值 setTimeout(() => { document.body.setAttribute('class', 'body') }, 2000) 复制代码
回调函数中的 MutationRecord 实例
上面 console.log('mutationRecord = ', mutationRecord)
的输出结果如下:
mutationRecord = [ { addedNodes: NodeList [], attributeName: "class", attributeNamespace: null, nextSibling: null, oldValue: null, previousSibling: null removedNodes: NodeList [], target: body.body type: "attributes" } ] 复制代码
下面是每个属性对应的解释:
target —— 被修改影响的目标 dom 节点
type —— 表示变化的类型,也就是 MutationObserverInit 对象中的三种:"attributes"、"characterData" 或 "childList"
attributeName —— 针对 "attributes" 类型的变化时,保存被修改属性的名字
attributeNamespace —— 对于使用了命名空间的 "attributes" 类型的变化,保存被修改属性的名字,其他变化事件会将这个属性设置为 null
oldValue —— 如果在 MutationObserverInit 对象中启用(attributeOldValue 或 characterData OldValue 为 true),则 "attributes" 或 "characterData" 的变化事件会设置这个属性为被替代的值;"childList" 类型的变化始终将这个属性设置为 null
addedNodes —— 针对 "childList" 类型的变化,返回包含变化中添加节点的 NodeList,其他变化事件会将这个属性设置为空 NodeList 数组
previousSibling —— 对于 "childList" 类型的变化,返回包含变化中删除节点的 NodeList,默认为空 NodeList
nextSibling —— 对于 "childList" 类型的变化,返回变化节点的后一个同胞 Node,默认为 null
removedNodes —— 对于"childList"类型的变化,返回变化节点的前一个同胞 Node,默认为 null
disconnect() 方法
默认情况下,只要被观察的元素不被垃圾回收,MutationObserver 的回调就会响应 DOM 变化事 件,从而被执行。要提前终止执行回调,可以调用 disconnect() 方法。
直接看下面的例子:
let observer = new MutationObserver((mutationRecord, mutationObserver) => { console.log(mutationRecord); console.log(mutationObserver); }) observer.observe(document.body, { attributes: true }); // 位置1 observer.disconnect(); setTimeout(() => { // 位置2 // observer.disconnect(); document.body.setAttribute('class', 'body'); // 位置3 // observer.disconnect(); }, 2000); // 位置4 // observer.disconnect(); 复制代码
上面我们把 observer.disconnect()
分别放在 位置 1、2、3、4
,但实际上它们的效果都是一样的,都会直接终止执行回调,要想让已经加入任务队列的回调执行,可以利用事件循环机制,比如:区分同步和异步修改,然后在异步操作中调用 disconnect() ,保证让已经入列的回调执行完毕。
takeRecords() 方法
调用 MutationObserver 实例的 takeRecords() 方法可以清空记录队列,取出并返回包含其中的所有 MutationRecord 实例的数组。
使用场景: 希望断开与观察目标的联系,但又希望获取调用 disconnect() 而被抛弃的记录队列中的 MutationRecord,这样即使已经断开关联,也能继续处理后续操作。
// 1. 未调用 takeRecords() let observer = new MutationObserver( (mutationRecord, mutationObserver) => { console.log('body had mutated!!!') console.log(mutationRecord); // [MutationRecord, MutationRecord, MutationRecord] }, ) observer.observe(document.body, { attributes: true }) document.body.className = 'body1' document.body.className = 'body2' document.body.className = 'body3' // 2. 调用 takeRecords() let observer = new MutationObserver( // 这个回调函数不再执行,因为已经通过 observer.takeRecords 获取到了 mutationRecord (mutationRecord, mutationObserver) => { console.log('body had mutated!!!') console.log(mutationRecord); // [MutationRecord, MutationRecord, MutationRecord] }, ) observer.observe(document.body, { attributes: true }) document.body.className = 'body1' document.body.className = 'body2' document.body.className = 'body3' console.log(observer.takeRecords()); // 这里输出 [MutationRecord, MutationRecord, MutationRecord] console.log(observer.takeRecords()); // 上面获取到集合之后,再次获取,此时已经被清空,输出: [] 复制代码
复用 MutationObserver 对象
多次调用 observe() 方法,可以复用一个 MutationObserver 对象观察多个不同的目标节点。此时,MutationRecord 的 target 属性可以标识发生变化事件的目标节点。
let h1 = document.createElement('h1') let h2 = document.createElement('h2') let observer = new MutationObserver( (mutationRecord, mutationObserver) => { console.log(mutationRecord) }, ) // 初次检测 observer.observe(h1, { attributes: true, }) // 再次检测 observer.observe(h2, { attributes: true, }) h1.className = 'h1' h1.textContent = 'this is h1' h2.className = 'h2' h2.textContent = 'this is h2' // 即使没有把 h1 和 h2 节点添加的文档中,上面的对 className 的修改,也可以触发回调执行 document.body.appendChild(h1) document.body.appendChild(h2) 复制代码
observer.disconnect() 方法调用之后,所有和 observer 关联的 dom 就全部断开,但是后续可以继续使用 observer.observe() 方法重新关联。
MutationObserver 回调与记录队列
MutationObserver 接口是出于性能考虑而设计的,其核心是异步回调与记录队列模型。为了在大量变化事件发生时不影响性能,每次变化的信息(由 oberver 实例决定)会保存在 MutationRecord 实例中,然后添加到记录队列。
记录队列对每个 MutationObserver 实例都是唯一的,是所有 DOM 变化事件的有序列表。
根据下面的例子来简单理解,下面的 body 元素虽然被连续修改 2 次,但是我们注册的回调函数不会被执行 2 次,而是把 2 次操作的信息分别放到 MutationRecord 的实例中,并通过数组进行保存,这样就保证了多次修改的内容都能在一次回调执行中获取到。
// 实例化 observer 对象并注册回调 let observer = new MutationObserver((mutationRecord, mutationObserver) => { console.log(mutationRecord);// 这里输出的是两次修改的集合 }) // 将 observer 与 dom 进行关联 observer.observe(document.body, { attributes: true }); // 连续两次修改属性值 document.body.className = "body1"; document.body.className = "body2"; 复制代码
使用 MutationObserver 仍然是有代价
虽然在上面说了不少 MutationObserver 的优势,但是应该要理解为是与旧的MutationEvent 相比的情况下,因为 MutationObserver 本身还是存在缺点的。 这也就是为什么 vue 源码中没有直接使用它的原因,当然在 vue 中它是仅次于 promise 的,因为 MutationObserver 和 Promise 一样属于微任务,能够被事件循环尽快执行。
MutationObserver 的引用
MutationObserver 对要观察的目标节点的引用属于弱引用,所以不会妨碍垃圾回收程序回收目标节点
目标节点对 MutationObserver 的引用属于强引用。如果目标节点从 DOM 中被移除,随后被垃圾回收,则关联的 MutationObserver 也会被垃圾回收。
MutationRecord 的引用
记录队列中的每个 MutationRecord 实例至少包含对已有 DOM 节点的一个引用,即里面的 target 属性,如果变化是 childList 类型,则会包含多个节点的引用
记录队列和回调处理的默认行为是耗尽这个队列,处理每个 MutationRecord,然后让它们超出作用域并被垃圾回收
有时候可能需要保存某个观察者的完整变化记录,那么就保存所有的 MutationRecord 实例,也就会保存它们引用的节点,而这会妨碍这些节点被回收
如果需要尽快地释放内存,可以从每个 MutationRecord 中抽取出最有用的信息,保存到一个新对象,然后释放 MutationRecord 中的引用
最后
既然开头提到了 vue2 源码中对 MutationObserver 的使用,其实也就是和 nextTick 源码相关的部分,那么在这就简单的总结一下:
先定义了一个 callbacks 存放所有的 nextTick 里的回调函数
然后判断当前环境是否支持 Promise,如果支持,就用 Promise 来触发回调函数
如果不支持 Promise 就判断是否支持 MutationObserver,通过观察文本节点发生变化,去触发执行所有异步回调函数
如果不支持 MutationObserver 就判断是否支持 setImmediate,如果支持,就通过setImmediate 来触发回调函数
如果以上都不支持就只能用 setTimeout 来完成异步执行
延迟调用优先级如下:
Promise > MutationObserver > setImmediate > setTimeout
如果想了解 事件循环机制 和 Promise 的内容可以参考之前的文章:
作者:MA
链接:https://juejin.cn/post/7036733000565915655
伪原创工具 SEO网站优化 https://www.237it.com/