本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
前言
浏览器通过事件循环机制使页面“活”起来,在事件循环中宏任务和微任务有不同的执行时机,而浏览器基于微任务的技术有 MutationObserver
、Promise
以及以 Promise
为基础开发出来的很多其他的技术。
前面的文章也花了很大的篇幅介绍 Promise
,那么这篇文章就带大家了解一下 MutationObserver
这个微任务是什么?用来做什么的吧!
MutationObserver
是用来监听 DOM 变化的一套方法,而监听 DOM 变化一直是前端工程师一项非常核心的需求。比如很多 Web 应用都利用 HTML 与 JavaScript 构建其自定义控件,与一些内置控件不同,这些控件不是固有的。为了与内置控件一起良好地工作,这些控件必须能够适应内容更改、响应事件和用户交互。因此,Web 应用需要监视 DOM 变化并及时地做出响应。
MutationObserver
是现在监听 DOM 变化的方法,那么在开始的时候是怎么监听的呢,了解监听 DOM 方法的演变有助于我们更加深入地理解浏览器是怎样运行的。
早期轮询检测
在早期,浏览器并没有提供对监听 DOM 的支持,所以那个时候要观察 DOM 是否变化,唯一能做的便是 轮询检测,比如使用 setTimeout
或者 setInterval
来定时检测 DOM 是否有改变。
这种方式简单粗暴,但是会遇到两个问题:
- 如果时间间隔设置过长,DOM 变化 响应不够及时;
- 反过来如果时间间隔设置过短,又会 浪费很多无用的工作量 去检查 DOM,会让页面变得低效。
Mutation Event
在 2000 年的时候引入了 Mutation Event
,它是在 DOM3 中定义的用于监听 DOM 树结构变化的事件,不过由于该事件存在兼容性以及性能上的问题已经被弃用。
Mutation Event
总共有7种事件:DOMNodeInserted
、DOMNodeRemoved
、DOMSubtreeModified
、DOMAttrModified
、DOMCharacterDataModified
、DOMNodeInsertedIntoDocument
和DOMNodeRemovedFromDocument
。
简单用法如下:
let box = document.getElementById('box')
box.addEventListener("DOMSubtreeModified", function () {
console.log('box 元素被修改');
}, false);
复制代码
Mutation Event
采用了 观察者的设计模式,当 DOM 有变动时就会立刻触发相应的事件,这种方式属于 同步回调。
采用 Mutation Event
解决了 实时性 的问题,因为 DOM 一旦发生变化,就会立即调用 JavaScript 接口。但是 这种实时性造成了严重的性能问题,因为每次 DOM 变动,渲染引擎都会去调用 JS,这样会产生较大的性能开销。
比如利用 JS 动态创建或动态修改 50
个节点内容,就会触发 50
次回调,而且每个回调函数都需要一定的执行时间,这里我们假设每次回调的执行时间是 4ms
,那么 50
次回调的执行时间就是 200ms
,若此时浏览器正在执行一个动画效果,由于 Mutation Event
触发回调事件,就会导致动画的卡顿。
也正是因为使用 Mutation Event
会导致页面性能问题,所以 Mutation Event
被反对使用,并逐步从 Web 标准事件中删除了。
MutationObserver
MutationObserver
API 可以用来监视 DOM 的变化,包括属性的变化、节点的增减、内容的变化等。
MutationObserver
的使用
参考 MutationObserver
的 MDN 官方文档资料
MutationObserver
是一个构造器,用来实例化一个 Mutation
观察者对象,参数是一个回调函数,这个回调函数会在指定的 DOM 节点发送变化后执行,回调函数有两个参数:
mutations
:节点变化记录数组(MutationRecord
)observer
:观察者对象本身
let observe = new MutationObserver(function (mutations, observer) {});
复制代码
MutationObserver
实例对象有三个方法,如下:
observe
:配置MutationObserver
在 DOM 更改匹配给定选项时,通过其回调函数开始接收通知。即设置观察目标,接受两个参数:target
:观察目标;options
:通过对象成员来设置观察选项
disconnect
:阻止MutationObserver
实例继续接收的通知,直到再次调用其observe()
方法,该观察者对象包含的回调函数都不会再被调用。takeRecords
:从MutationObserver
的通知队列中删除所有待处理的通知,并将它们返回到MutationRecord
对象的新Array
中。即清空记录队列并返回里面的内容。
使用实例:
// 选择需要观察变动的节点
const targetNode = document.getElementById('box');
// 观察器的配置(需要观察什么变动)
const config = {
attributes: true,
childList: true,
subtree: true
};
// 当观察到变动时执行的回调函数
const callback = function (mutationsList, observer) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('有节点发生改变,当前节点的内容是:' + mutation.target.innerHTML);
} else if (mutation.type === 'attributes') {
console.log('修改了' + mutation.attributeName + '属性');
}
}
};
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);
// 之后,可停止观察
// observer.disconnect();
复制代码
MutationObserver
的改进优化
- 首先,
MutationObserver
将响应函数改成异步调用,可以不用在每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的 DOM 变化。这样即使频繁地操纵 DOM,也不会对性能造成太大的影响。 - 在每次 DOM 节点发生变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加进当前的微任务队列中。这样当执行到检查点的时候,V8 引擎就会按照顺序执行微任务。
综上所述,MutationObserver
采用了 异步 + 微任务 的策略来实现监听 DOM 的变化。
- 通过异步操作解决了同步操作的性能问题;
- 通过微任务解决了实时性的问题。
MutationObserver
和 Vue 中的 nextTick
Vue 中 nextTick
可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM。
那在 Vue 中是怎么实现 nextTick
的呢?
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher
被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
而异步回调我们知道有宏任务(macrotasks
)和微任务(microtasks
)两种,那为了让 nextTick
更快的执行,那肯定是优先选择微任务(microtasks
)的。要创建一个新的微任务(microtask
),会优先使用 Promise
,如果浏览器不支持,再尝试 MutationObserver
。实在不支持,就只能用 setTimeout
这个宏任务了。
Vue 中的异步更新队列 是这样说的:
至于 MutationObserver
是怎么模拟 nextTick
的,可以看 源码,其实就是创建一个 TextNode
并监听内容变化,然后要 nextTick
的时候去改一下这个节点的文本内容:
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
复制代码
总结
这篇文章介绍了监听 DOM 变化技术方案的演化史,从轮询到 Mutation Event
再到最新使用的 MutationObserver
。MutationObserver
方案的核心就是采用微任务机制,有效地权衡了实时性和执行效率的问题。
最后还简单介绍了 MutationObserver
和 Vue 中 nextTick
的关系。