参考文献
vue nextTick深入理解-vue性能优化、DOM更新时机、事件循环机制
全面解析Vue.nextTick实现原理
这一次,彻底弄懂 JavaScript 执行机制
前言
- 因为js是单线程运行的,所以在代码执行的过程中,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
- 首先在执行同步代码的时候,如果遇到了异步事件(比如访问一个接口),js引擎会将这个异步事件挂起,继续执行执行栈中的下面代码。当异步任务执行完毕后,会将异步事件对应的回调函数(处理后台返回的数据)加入到队列中,
这就是为什么异步任务无法保证回调函数的执行顺序,哪个异步任务执行快,将会优先其回调函数放入队列执行
,队列又分为微任务队列,宏任务队列。 - 当当前执行栈中的事件执行完毕后,js引擎首先会判断微任务队列中是否有任务可执行,如果有则将其队首的事件压入执行栈中执行。
- 执行完微任务队列中的所有任务后即为一个事件循环
- 接着再去判断宏任务队列中是否有任务,开启新的一个事件循环
主线程的执行过程就是一个tick
一、定义[nextTick、事件循环]
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
Vue源码阅读一:说说vue.nextTick实现
nextTick的由来:
由于Vue的数据驱动视图更新是异步的,即修改数据的当下,视图不会立即更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。也就是说该方法会将watcher.update()
集中在下一个tick进行渲染,这样做的目的是减少性能耗损
nextTick的触发时机:
在同一事件循环中的数据变化后,DOM完成更新,执行nextTick(callback)内的回调。
应用场景:
需要在视图更新之后,基于新的视图进行操作。
Vue.component('example', {
template: '<span>{{ message }}</span>',
data: function () {
return {
message: '未更新'
}
},
methods: {
updateMessage: function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '已更新'
})
}
}
})
- 在created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中。原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题 。
- mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted:
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the
// entire view has been rendered
})
}
内部实现原理
每一次事件循环都包含一个microtask队列,在循环结束后会依次执行队列中的microtask并移除,然后再开始下一次事件循环。
在执行microtask的过程中后加入microtask队列的微任务,也会在下一次事件循环之前被执行。也就是说,macrotask总要等到microtask都执行完后才能执行,microtask有着更高的优先级。
microtask的这一特性,简直是做队列控制的最佳选择啊!vue进行DOM更新内部也是调用nextTick来做异步队列控制。而当我们自己调用nextTick的时候,它就在更新DOM(异步的)的那个microtask后追加了我们自己的回调函数,从而确保我们的代码在DOM更新后执行
内部实现过程:
- 首先想到最优的microtask策略就是
Promise
,而令人尴尬的是,Promise是ES6新增的东西,也存在兼容问题呀~ 所以vue就面临一个降级策略。降级为macrotask来做队列控制 - 接着
MutationObserver
(也是microtask微任务),但vue在2.5版本中已经删去了MO相关的代码,因为它是HTML5新增的特性,在iOS上尚有bug。 - 然后是
setImmediate
,可惜的是只有IE和nodejs支持 - 然后是
MessageChannel
的onmessage
回调也是microtask,但也是个新API,面临兼容性的尴尬… - 最后最后的兜底方案就是
setTimeout
了,尽管它有执行延迟,可能造成多次渲染,算是没有办法的办法了。