思考
什么是nextTick?
为什么要用它?
什么是nextTick?
vue官方给出的: 在下一次DOM更新循环结束之后的延迟回调。再修改数据之后立即使用这个方法,获取更新过后的DOM
首先了解一下JS的运行机制
(MutationObserver是HTML5新增的属性,用于监听DOM修改事件,能够监听到节点的属性、文本内容、子节点等的改动,是一个功能强大的利器)
为什么要用它?
1. Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中,原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载已完成。
2.当项目中你想在改变DOM元素的数据后基于新的dom做点什么,对新DOM一系列的js操作都需要放进Vue.nextTick()的回调函数中;通俗的理解是:更改数据后当你想立即使用js操作新的视图的时候需要使用它
3.在使用某个第三方插件时 ,希望在vue生成的某些dom动态发生变化时重新应用该插件,也会用到该方法,这时候就需要在 $nextTick 的回调函数中执行重新应用插件的方法。
总结
vue就是这样的思路,并不是用MO进行DOM变动监听,而是用队列控制的方式达到目的。那么vue又是如何做到队列控制的呢?我们可以很自然的想到setTimeout,把nextTick要执行的代码当作下一个task放入队列末尾。
然而事情却没这么简单,vue的数据响应过程包含:数据更改->通知Watcher->更新DOM。而数据的更改不由我们控制,可能在任何时候发生。如果恰巧发生在repaint之前,就会发生多次渲染。这意味着性能浪费,是vue不愿意看到的。
所以,vue的队列控制是经过了深思熟虑的(也经过了多次改动)。在这之前,我们还需了解event loop的另一个重要概念,microtask.
microtask的这一特性,简直是做队列控制的最佳选择啊!vue进行DOM更新内部也是调用nextTick来做异步队列控制。而当我们自己调用nextTick的时候,它就在更新DOM的那个microtask后追加了我们自己的回调函数,从而确保我们的代码在DOM更新后执行,同时也避免了setTimeout可能存在的多次执行问题。
常见的microtask有:Promise、MutationObserver、Object.observe(废弃),以及nodejs中的process.nextTick.
咦?好像看到了MutationObserver,难道说vue用MO是想利用它的microtask特性,而不是想做DOM监听?对喽,就是这样的。核心是microtask,用不用MO都行的。事实上,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的官方文档中详细解释
Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的Promise.then和MessageChannel,如果执行环境不支持,会采用setTimeout(fn, 0)代替。
例如,当你设置vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数情况我们不需要关心这个过程,但是如果你想在 DOM 状态更新后做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用Vue.nextTick(callback)。这样回调函数在 DOM 更新完成后就会调用。