第一次听说MutationObserver
的时候,大家应该都和我是一样的,通过Vue
的nextTick
的实现来认识的;
后来又有一个面试题,说是如何将一个任务放到微队列中执行,网上的解答方案中也会出现MutationObserver
;
但是直到现在我都很少看到有人去讲这个API
,当我深入这个API
之后,我发现这个API
真很有用,所以他不讲,你不讲,那我来讲吧。
什么是 MutationObserver
可以看MDN
的介绍MutationObserver:
简单点来说,MutationObserver
是一个构造函数,可以用来监听某个节点的变化,当节点发生变化时,可以执行一些回调函数。
但是它不会立即执行,首先你需要调用MutationObserver
的observe
方法,传入你想要监听的节点,以及一些配置,然后当节点发生变化时,就会执行你传入的回调函数。
MutationObserver 的使用
MutationObserver
的使用非常简单,直接看代码:
js
const observer = new MutationObserver((mutations) => {
console.log(mutations);
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
characterData: true,
});
上面的代码中,我们创建了一个MutationObserver
实例,然后调用observe
方法,传入了document.body
,以及一些配置,然后当document.body
发生变化时,就会执行我们传入的回调函数。
知道大家都不爱看文档,那我就直接开始整活了。
Vue 的 nextTick
下面是Vue2
的nextTick
的实现源码,地址:next-tick.js:
可以看到我框出来的地方,使用的就是MutationObserver
,传入的回调函数是flushCallbacks
;
接着创建了一个文本节点,然后调用observe
方法,传入了这个文本节点,配置了characterData
为true
,这样当文本节点发生变化时,就会执行flushCallbacks
。
源码细节就不解释了,大家可以自己去看;
Vue3
已经全面拥抱Promise
了,所以Vue3
的nextTick
的实现就是直接使用Promise
了,源码地址:scheduler.ts;
将任务放到微队列中执行
根据上面Vue
的nextTick
的实现,可以看到有一个标识isUsingMicroTask
,见名知意,就是用来标识是否使用微队列的;
可以看到的是只有两种方式将任务放到微队列中执行,一种是Promise
,一种是MutationObserver
;
下面是根据
Vue2
源码的实现,弄了一个非常简化的示例;
js
const fn = () => {
console.log('fn');
};
// 优先使用 Promise
if (typeof Promise !== 'undefined') {
Promise.resolve().then(fn);
}
// 降级使用 MutationObserver
else if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(fn);
const textNode = document.createTextNode('1');
observer.observe(textNode, {
characterData: true,
});
textNode.data = '2';
}
// 再降级使用 setImmediate
else if (typeof setImmediate !== 'undefined') {
setImmediate(fn);
}
// 最后降级使用 setTimeout
else {
setTimeout(fn, 0);
}
其实看源码是可以学到很多东西的,比如Vue
的nextTick
的实现,上面写了很多的注释,我们把注释翻译一下看看:
优先使用 Promise
The nextTick behavior leverages the microtask queue, which can be accessed via either native Promise.then or MutationObserver. MutationObserver has wider support, however it is seriously bugged in UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It completely stops working after triggering a few times... so, if native Promise is available, we will use it:
翻译如下:
nextTick
的执行利用了可以访问微队列的Promise.then
或MutationObserver
原生模块;MutationObserver
的兼容性更好,但是在iOS >= 9.3.3
的UIWebView
中, 它在触摸事件处理程序中触发时会出现严重的错误,触发几次后就会完全停止工作; 所以如果原生的Promise
可用,我们将使用它:
后面还有一段注释,用来修正Promise
的一些问题:
In problematic UIWebViews, Promise.then doesn't completely break, but it can get stuck in a weird state where callbacks are pushed into the microtask queue but the queue isn't being flushed, until the browser needs to do some other work, e.g. handle a timer. Therefore we can "force" the microtask queue to be flushed by adding an empty timer.
翻译如下:
在有问题的
UIWebView
中,Promise.then
不会完全崩溃,但是它可能会陷入一种奇怪的状态, 在这种状态下,回调被推入微任务队列,但是队列没有被刷新,直到浏览器需要做一些其他的工作, 比如处理一个定时器;因此我们可以通过添加一个空的定时器来"强制"刷新微任务队列。
降级使用 MutationObserver
Use MutationObserver where native Promise is not available, e.g. PhantomJS, iOS7, Android 4.4 (#6466 MutationObserver is unreliable in IE11)
翻译如下:
在原生的
Promise
不可用的情况下使用MutationObserver
, 比如PhantomJS
、iOS7
、Android 4.4
; (#6466 MutationObserver
在IE11
中不可靠)
再降级使用 setImmediate
Fallback to setImmediate. Technically it leverages the (macro) task queue, but it is still a better choice than setTimeout.
翻译如下:
回退到
setImmediate
; 从技术上讲,它利用了(宏)任务队列, 但它仍然是比setTimeout
更好的选择。
最后降级使用 setTimeout
Fallback to setTimeout.
就不翻译了,很无奈的一句话。
Node 环境下的微队列
在网上还看到有说Node
环境下的微队列,面试题嘛,就得回答的全面一点,要考虑不同的环境;
Node
环境下是不支持MutationObserver
的,其他的都有,但是Node
环境下还支持process.nextTick
,所以加一个判断就好了:
js
if (typeof process !== 'undefined' && process.nextTick) {
process.nextTick(fn);
}
MutationObserver 还有其他什么作用?
上面讲到了可以通过MutationObserver
将任务放到微队列中执行,但是其他情况下,我们好像很少用到MutationObserver
;
其实主要是因为我们很少去监听节点的变化,而大多数情况我们需要知道DOM
发现变化的情况,通常都是会使用一些钩子函数来进行处理;
例如我们要知道文本框的值发生了变化,我们会使用oninput
事件,而不是去监听文本框的变化;
如果我们动态去切换主题,我们会有切换的函数,然后可以通过这个函数调用来通知主题发生了变化,而不是去监听DOM
的方式来通知主题发生了变化;
都被钩子之类的函数给取代了,那它还有用武之地吗?
我就推荐几种使用场景吧:
监听子节点的变化
在使用Vue
的时候我们经常会遇到一种场景,就是我们获取不到某个节点,因为这个节点还没挂载上去,这个时候我们通常会使用nextTick
来获取这个节点;
js
const dom = ref(null);
nextTick(() => {
console.log(dom.value);
});
这样肯定是可以获取到的,但是如果这个dom
节点的内容是动态的,我们想要获取修改之后的内容;
或者说这个节点发生变化我们需要做一些事情怎么办,通常是封装一个方法,然后如下:
js
const dom = ref(null);
const data1 = ref(null);
const data2 = ref(null);
const data3 = ref(null);
const handleChangeData = () => {
nextTick(() => {
console.log(dom.value);
});
};
const fetchData1 = () => {
fetch('xxx').then(res => {
data1.value = res.data;
handleChangeData();
});
};
const fetchData2 = () => {
fetch('xxx').then(res => {
data2.value = res.data;
handleChangeData();
});
};
const fetchData3 = () => {
fetch('xxx').then(res => {
data3.value = res.data;
handleChangeData();
});
};
会玩一点的可能会使用useEffect
来监听data1
、data2
、data3
的变化,然后调用handleChangeData
,这里就不展开了;
但是如果我们使用MutationObserver
的话,就可以直接监听dom
节点的变化,然后执行handleChangeData
,代码如下:
js
const dom = ref(null);
const observer = new MutationObserver(() => {
console.log(dom.value);
});
const handleChangeData = () => {
nextTick(() => {
console.log(dom.value);
});
};
// 需要在 dom 节点挂载之后执行
nextTick(() => {
observer.observe(dom.value, {
childList: true,
subtree: true,
attributes: true,
characterData: true,
});
});
这样就算再加几个data
,也不需要再去调用handleChangeData
了,直接监听dom
节点的变化就好了。
监听 DOM 被移除
有时候有些网站会加上一些水印,或者一些广告,作为一个站主肯定是不希望这些东西被人使用一些脚本给移除掉的;
这个时候就可以使用MutationObserver
来监听DOM
被移除,然后重新添加上去,代码如下:
js
const watermark = document.createElement('div');
watermark.innerHTML = '水印';
document.body.appendChild(watermark);
const observer = new MutationObserver(() => {
if (!document.body.contains(watermark)) {
document.body.appendChild(watermark);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
也可以通过这个方式来监听是否被添加
DOM
,可能你的站点上的广告被人替换了,也有可能是你想要监听某个DOM
是否被添加上去了,都可以使用这个方式。
监听 DOM 的属性变化
有时候我们需要监听某个DOM
的属性变化,比如说我们要监听DOM
的style
属性,然后根据style
属性的变化来做一些事情,代码如下:
js
const dom = document.querySelector('.dom');
const observer = new MutationObserver((mutations) => {
console.log(mutations);
});
observer.observe(dom, {
attributes: true,
attributeFilter: ['style'],
});
总结
MutationObserver
是一个很有用的API
,它主要可以用于监听某个节点的变化,当节点发生变化时,可以执行一些回调函数;
其中的节点变化包括:子节点的变化、属性的变化、文本内容的变化等等;
可根据不同的配置项来监听不同的变化,可以说是非常灵活了;
MutationObserver
还有很多其他的用法,主要作用还是监听dom
的变化;
例如还可以做监控,元素发生变化记录日志的变化;跟踪一些元素的变化,例如广告,采取相应的措施;做输入检测,虽然说可以用input
事件代替;
当然肯定还有其他的应用场景,感兴趣的可以自行探索。