Vue 中如何使用 MutationObserver 做批量处理?

明白为什么settimeout为0,可以做到同步的setter全部执行完毕以后,在下一次tick中批量执行去重后的watch。问题是mutationObserver如何做到nextTick这点?这不是用来监听节点变化的么? mutationObserver如果可以做到nexttick的效果,那么和settimeout的的区别在哪里。为什么优先使用MutationObserver。
关注者
193
被浏览
10256

8 个回答

更新一下,现在 Vue 的 nextTick 实现移除了 MutationObserver 的方式(兼容性原因),取而代之的是使用 MessageChannel。


Jake Archibald 有一篇介绍 task 和 microtask 的文章,可以了解一下:Tasks, microtasks, queues and schedules

JS 的 event loop 执行时会区分 task 和 microtask,引擎在每个 task 执行完毕,从队列中取下一个 task 来执行之前,会先执行完所有 microtask 队列中的 microtask。

setTimeout 回调会被分配到一个新的 task 中执行,而 Promise 的 resolver、MutationObserver 的回调都会被安排到一个新的 microtask 中执行,会比 setTimeout 产生的 task 先执行。要创建一个新的 microtask,优先使用 Promise,如果浏览器不支持,再尝试 MutationObserver。实在不行,只能用 setTimeout 创建 task 了。为啥要用 microtask?根据HTML Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 microtask 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了。反之如果新建一个 task 来做数据更新,那么渲染就会进行两次。(当然,浏览器实现有不少不一致的地方,上面 Jake 那篇文章里已经有提到。)

至于 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)
    }
实际上不是用 "使用MutationObserver做批量处理", 而是利用 MutationObserver 来做异步回调. 在 js 里, 如果你不用异步 API 你就没法实现异步(废话), 那么问题就在于, 具体用什么异步 API 来实现异步呢?

Vue 关于 nextTick() , 用到了很多种异步 API, 就是因为 setTimeout() 的间隔太大. 原本 setTimeout/setInterval 的 timer 不小于 4ms 这个行为是 vendor specific 的, 但后来被 HTML5 标准化了. 而 vue 用这么多奇技淫巧, 还是想减少这个异步的间隔.

按照 mozilla 社区的推荐, 用 window.postMessage() 就可以了, 但根据 @尤雨溪 的说法, postMessage() 在 macrotask queue 里, 比 MutationObserver 的 microtask queue 晚.
setTimeout with a shorter delay
New timerFunc in rc7 is laggy · Issue #3771 · vuejs/vue