this.$nextTick(() => ‘blablabla’)。。。
前言
在之前的项目中,因为经常会用的一些可视化库。所以就不必然的会在数据接收之后操作dom。当初年少无知的我经常因为dom还未“更新”去踩坑甚至到怀疑人生。。。。虽然有些“云程序猿”给了个$(function(){})的解决方法。本着不装逼非程序猿的思维感觉还是有点小low。俗话说世间万物皆有因果,只能怪我当初太年轻没好好看文档。。。
nextTick作用是什么
首先我们知道vue的dom更新是异步的。而nextTick则是利用事件循环在dom更新(异步)完毕后再执行。这样其实就解决了上面的提出的获取不到数据变化后dom的问题。
关于异步更新队列
官网关于异步更新队列的说明:可能你还没有注意到,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 更新完成后就会调用
事件循环?也就是事件队列也就联系到了宏任务微任务也就可以联系到了setTimout等等
过滤watcher?不难想到上一篇关于双向绑定的watcher里一笔带过的watcher过滤(其实就是根据watcher的id的判断,虽然代码里也没写)。。。
带着这两个小问题我们看看vue nextTick源码的实现。
还是要看watcher
背景
1 | export default { |
首先我们知道当数据变化会触发监听器的set方法 => 触发订阅器dep的notfiy方法 => 触发订阅者watcher的update方法 => 触发this.run执行相应回调 => 然后依次打印1,2,3。为什么现在却只打印一个3?
qunueWatcher的update
实际上watcher的update方法:1
2
3
4
5
6
7
8
9
10
11this.deep = this.user = this.lazy = this.sync = false
...
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run() //同步。。。
} else {
queueWatcher(this) //异步推送到观察者队列中,即异步更新调用这个方法。
}
}
我们发现其实update做的只是异步推送到观察者队列(缓存了msg的改变,异步调用时直接取到最后的值)。我们再看queueWatcher这个方法:
queueWatcher
1 | export function queueWatcher (watcher: Watcher) { |
从上面可以看出waiting其实就是控制在下一个tick时调用异步刷新视图的标识,而控制什么时候是下一个tick就是nextTick方法,刷新视图则是flushSchedulerQueue方法。所以要看看flushSchedulerQueue和nextTick这两个方法
nextTick(flushSchedulerQueue)
flushSchedulerQueue
flushSchedulerQueue方法 其实就是实际执行的watch的run来更新视图1
2
3
4
5
6
7
8
9
10
11
12function flushSchedulerQueue () {
flushing = true
let watcher, id
...
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
watcher.run() //执行的watch视图更新
...
}
}
nextTick
重点则是nexttick方法
1 | export const nextTick = (function () { |
所以nextTick方法其实就是看timerfunc方法何时调用而timerfunc则要根据事件循环的定义顺序,在vue中定义的顺序是优先microtask然后macrotask。microtask最佳选择是Promise其次是MutationObserver(h5新增的特性在ios上有些不兼容),macrotask最佳方案是setImmediate(仅仅在ie和nodeJs环境支持)其次也就是setTimeout了(虽然有4ms延迟)。
microtask与macrotask执行顺序排序
当一个程序有:setTimeout(macro), setInterval(macro) ,setImmediate(macro), I/O(macro), UI渲染(macro),Promise(micr0) ,process.nextTick(micr0), Object.observe(micr0), MutationObserver(micr0)的时候执行顺序是:
1
2
3宏:I/O => UI渲染
微:process.nextTick => Promise => MutationObserver => Object.observe(废弃)
新的循环 宏:setImmediate => setTimeout,setInterval
总结:
- nextTick作用:保证了更新视图操作DOM的动作是在当前栈执行完以后下一个tick的时候调用(拿到了更新后的dom),缓存了数据变化以及过滤了watcher队列从而大大优化了性能
- nextTick原理:用异步队列的方式来控制DOM更新和nextTick回调先后执行。microtask因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕。因为兼容性问题,vue不得不做了microtask向macrotask的降级方案。