这个问题考察对vue异步更新队列/异步更新策略的理解
答题思路:
- nextTick 是啥?下一个定义
- 为什么需要用它? 用异步更新队列实现原理解释
- 我在什么地方用它呢? 想想你平时开发中使用它的地方
- 下面介绍一下如何使用 nextTick
- 最后能说出源码实现的原理就会显得格外的优秀
示例:
- nextTick 是 vue 提供的一个全局 API,由于 vue 的异步更新策略导致我们对数据的修改不会立即体现在 dom 变化上,此时如果想要立即获取更新后的 dom 状态,就需要使用这个方法。
- vue 在更新 dom 时是异步执行的。只要侦听到数据变化,vue将开启一个队列,并缓存在同一事件循环中发生的所以数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick 方法会在队列中加入一个回调函数,确保该函数在前面的 dom 操作完成后才调用。
- 所以当我们想要在修改数据后立即看到 dom 执行结果就需要用到 nextTick 方法。
- 比如,我们在干什么的时候就会使用 nextTick,传一个回调函数进去,在里面执行 dom 操作即可。
- 我也有简单了解 nextTick 的实现,它会在 callback 里面加入我们传入的函数,然后用 timerFunc 异步方式调用它们,首选的异步方式会是 Promise。这让我明白了为什么可以在nextTick中看到 dom 操作结果。
先看看官方定义
nextTick官方文档的解释,它可以在DOM更新完毕之后执行一个回调
Vue.nextTick([callback, context])
在下次 DOM 更新循环结束后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
尽管MVVM框架并不推荐访问DOM,但有时候确实会有这样的需求,尤其是和第三方插件进行配合的 时候,免不了要进行DOM操作。而nextTick就提供了一个桥梁,确保我们操作的是更新后的DOM。
-
vue如何检测到DOM更新完毕呢?
- 能监听到DOM改动的API:MutationObserver
-
理解MutationObserver
- MutationObserver 是 HTML5 新增的属性,用于监听 DOM 修改事件,能够监听到节点的属性、文本内容、子节点等的改动,是一个功能强大的利器。
//MutationObserver基本用法
var observer = new MutationObserver(function(){
//这里是回调函数
console.log('DOM被修改了!');
});
var article = document.querySelector('article');
observer.observer(article);
-
vue 是不是用 MutationObserver 来监听 DOM 更新完毕的呢?
- vue的源码中实现nextTick的地方:
//vue@2.2.5 /src/core/util/env.js if (typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')) { 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) } } -
事件循环(EventLoop)
- 在js的运行环境中,通常伴随着很多事件的发生,比如用户点击、页面渲染、脚本执行、网络请求,等等。为了协调这些事件的处理,浏览器使用事件循环机制。
- 简要来说,事件循环会维护一个或多个任务队列(task queues),以上提到的事件作为任务源往队列中加入任务。有一个持续执行的线程来处理这些任务,每执行完一个就从队列中移除它,这就是一次事件循环。
for(let i=0; i<100; i++){
dom.style.left = i + 'px';
}
事实上,这100次 for 循环同属一个 task,浏览器只在该 task 执行完后进行一次 DOM 更新。
只要让 nextTick 里的代码放在 UI render 步骤后面执行,岂不就能访问到更新后的 DOM 了?
vue就是这样的思路,并不是用MO进行DOM变动监听,而是用队列控制的方式达到目的。那么 vue 又是如何做到队列控制的呢? 我们可以很自然的想到 setTimeout,把nextTick要执行的代码当作下一个task放入队列末尾。
vue 的数据响应过程包含:数据更改->通知Watcher->更新DOM。而数据的更改不由我们控制, 可能在任何时候发生。如果恰巧发生在重绘之前,就会发生多次渲染。这意味着性能浪费,是vue 不愿意看到的。
所以,vue 的队列控制是经过了深思熟虑的。在这之前,我们还需了解 event loop 的另一个重要概念,microtask。
-
microtask
- 从名字看,我们可以把它称为微任务。
- 每一次事件循环都包含一个 microtask 队列,在循环结束后会依次执行队列中的 microtask 并移除,然后再开始下一次事件循环。
- 在执行 microtask 的过程中后加入 microtask 队列的微任务,也会在下一次事件循环之前被执行。也就是说,macrotask 总要等到 microtask 都执行完后才能执行,microtask 有着更高的优先级。
- microtask 的这一特性,是做队列控制的最佳选择。vue 进行 DOM 更新内部也是调用 nextTick 来做异步队列控制。而当我们自己调用 nextTick 的时候,它就在更新 DOM 的那个 microtask 后追加了我们自己的回调函数,从而确保我们的代码在 DOM 更新后执行,同时也避免了 setTimeout 可能存在的多次执行问题。
- 常见的microtask有:Promise、MutationObserver、Object.observe(废弃),以及nodejs中的 process.nextTick。
- 看到了 MutationObserver,vue 用 MutationObserver 是想利用它的 microtask 特性,而不是想做 DOM 监听。核心是 microtask,用不用 MutationObserver 都行的。事实上,vue在2.5版本中已经删去了。
- MutationObserver 相关的代码,因为它是 HTML5 新增的特性,在 iOS 上尚有 bug。
- 那么最优的 microtask 策略就是 Promise 了,而令人尴尬的是,Promise 是 ES6 新增的东西,也存在兼容问题呀。所以 vue 就面临一个降级策略。
-
vue的降级策略
- 上面我们讲到了,队列控制的最佳选择是 microtask,而 microtask 的最佳选择是 Promise。但如果当前环境不支持 Promise,vue 就不得不降级为 macrotask 来做队列控制了。
- macrotask 有哪些可选的方案呢?前面提到了 setTimeout 是一种,但它不是理想的方案。因为 setTimeout执行的最小时间间隔是约4ms的样子,略微有点延迟。
- 在 vue2.5 的源码中,macrotask 降级的方案依次是:setImmediate、MessageChannel、setTimeout。setImmediate 是最理想的方案了,可惜的是只有 IE 和 nodejs 支持。
- MessageChannel 的 onmessage 回调也是 microtask,但也是个新 API,面临兼容性的尴尬。 所以最后的兜底方案就是 setTimeout 了,尽管它有执行延迟,可能造成多次渲染,算是没有办法的办法了。
总结
- 以上就是vue的nextTick方法的实现原理了,总结一下就是:
- vue用异步队列的方式来控制DOM更新和nextTick回调先后执行
- microtask因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
- 因为兼容性问题,vue不得不做了microtask向macrotask的降级方案