MutationObserver 都被传烂了,你怎么到现在都还不会?

第一次听说MutationObserver的时候,大家应该都和我是一样的,通过VuenextTick的实现来认识的;

后来又有一个面试题,说是如何将一个任务放到微队列中执行,网上的解答方案中也会出现MutationObserver

但是直到现在我都很少看到有人去讲这个API,当我深入这个API之后,我发现这个API真很有用,所以他不讲,你不讲,那我来讲吧。

什么是 MutationObserver

可以看MDN的介绍MutationObserver

简单点来说,MutationObserver是一个构造函数,可以用来监听某个节点的变化,当节点发生变化时,可以执行一些回调函数。

但是它不会立即执行,首先你需要调用MutationObserverobserve方法,传入你想要监听的节点,以及一些配置,然后当节点发生变化时,就会执行你传入的回调函数。

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

下面是Vue2nextTick的实现源码,地址:next-tick.js

可以看到我框出来的地方,使用的就是MutationObserver,传入的回调函数是flushCallbacks

接着创建了一个文本节点,然后调用observe方法,传入了这个文本节点,配置了characterDatatrue,这样当文本节点发生变化时,就会执行flushCallbacks

源码细节就不解释了,大家可以自己去看;

Vue3已经全面拥抱Promise了,所以Vue3nextTick的实现就是直接使用Promise了,源码地址:scheduler.ts

将任务放到微队列中执行

根据上面VuenextTick的实现,可以看到有一个标识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);
}

其实看源码是可以学到很多东西的,比如VuenextTick的实现,上面写了很多的注释,我们把注释翻译一下看看:

优先使用 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.thenMutationObserver原生模块; MutationObserver的兼容性更好,但是在iOS >= 9.3.3UIWebView中, 它在触摸事件处理程序中触发时会出现严重的错误,触发几次后就会完全停止工作; 所以如果原生的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, 比如PhantomJSiOS7Android 4.4; (#6466 MutationObserverIE11中不可靠)

再降级使用 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来监听data1data2data3的变化,然后调用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的属性变化,比如说我们要监听DOMstyle属性,然后根据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事件代替;

当然肯定还有其他的应用场景,感兴趣的可以自行探索。

相关推荐
廖子默6 分钟前
提供html2canvas+jsPDF将HTML页面以A4纸方式导出为PDF后,内容分页时存在截断的解决思路
前端·pdf·html
光影少年39 分钟前
react和vue图片懒加载及实现原理
前端·vue.js·react.js
AndyGoWei40 分钟前
react react-router-dom history 实现原理,看这篇就够了
前端·javascript·react.js
小仓桑44 分钟前
深入理解 JavaScript 中的 AbortController
前端·javascript
摸鱼也很难1 小时前
解决 node.js 执行 npm下载 报无法执行脚本的错
前端·npm·node.js
换个名字不能让人发现我在摸鱼1 小时前
裁剪保存的图片黑边问题
前端·javascript
PeterJXL1 小时前
pnpm:包管理的新星,平替 npm 和 yarn
前端·npm·node.js·pnpm
小牛itbull1 小时前
Mono Repository方案与ReactPress的PNPM实践
开发语言·前端·javascript·reactpress
黑色的糖果1 小时前
vue2封装自定义插件并上传npm发布及使用
前端·npm·node.js