这是我参与更文挑战的第13天,活动详情查看: 更文挑战。
前言
前面,我们已经大致梳理 Vue.js 里的整体流程和基本原理,接下来我们还会探究 Vue.js 实例或组件上的一些方法的原理和具体实现。
本次,我们将一起来探索 nextTick
。
vm.$nextTick
nextTick
接收一个回调函数作为参数,它的作用是将回调延迟到下次DOM更新周期之后执行。它与全局方法Vue.nextTick
一样,不同的是回调的this自动绑定到调用它的实例上。如果没有提供回调且在支持Promise的环境中,则返回一个 Promise
。
应用场景
我们在开发项目时会遇到一种场景:当更新了状态(数据)后,需要对新 DOM 做一些操作,但是这时我们其实获取不到更新后的DOM,因为还没有重新渲染。这个时候我们需要使用 nextTick 方法。
new Vue({
methods: {
example: function() {
this.message = 'changed';
this.$nextTick(function(){
// Dom 更新了
// this 指向当前实例
this.doSomethingElse()
})
}
}
})
在Vue.js中,当状态发生变化时,watcher
会得到通知,然后触发虚拟DOM的渲染流程。而 watcher 触发渲染这个操作并不是同步的,而是异步的。Vue.js 中有一个队列,每当需要渲染时,会将 watcher 推送到这个队列中,在下一次事件循环中再让 watcher 触发渲染的流程。
为什么Vue.js使用异步更新队列
我们之前学过,Vue.js 2.0 引入了虚拟DOM,变化侦测的通知只会发给组件。那么,如果我们同时修改了组件的两个状态,是不是组件就要重新渲染两次?这显示是不合理的。
如何解决重复渲染的问题?
我们只要把 watcher 实例添加到一个队列中缓存起来,并且在添加到队列时检查是否已经添加过了,只有不存在时,才将 watcher 实例添加到队列中。在下一次事件循环中,就可以依次触发 watcher 来重新渲染了。这样一来,在一个事件循环中,某个组件的有多个状态发生变化,这个组件也只会重新渲染 1 次。
异步任务
为了实现延迟运行的效果,我们需要使用到异步任务。Javascript 中的异步任务分为两种类型:微任务
和 宏任务
。
属于微任务的事件包括但不限于以下几种:
- Promise.then()
- MutationObserver
- Object.observe
- process.nextTick
属于宏任务的事件包括但不限于以下几种:
- setTimeout
- setInterval
- settImmediate
- MessageChannel
- requestAnimationFrame
- I/O
- UI 交互事件
什么是执行栈
当我们执行一个方法时,JavaScript会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中有这个方法的私有作用域、上层作用域的指向、方法的参数、私有作用域中定义的变量以及this对象。这个执行环境会被添加到一个栈中,这个栈就是执行栈。
回到前面的问题,“下次DOM更新周期” 的意思其实是下次微任务执行时更新DOM。而 vm.$nextTick
其实是将回调添加到微任务中。只有在特殊情况下才会降级成宏任务,默认会添加到微任务中。
注意 不论是更新DOM的回调还是使用 vm.$nextTick
注册的回调,都是向微任务队列中添加任务,所以哪个任务先添加到队列中,就先执行哪个任务。
nextTick 的内部注册流程和执行流程
graph
subgraph 执行流程
b[任务被执行] --> c[依次执行callbacks中的所有回调] --> d[清空callbacks]
end
subgraph 注册流程
n[nextTick] --> s[将回调函数添加到callbacks中]
s --> t{本轮事件循环<br>中第一次使用<br>nextTick?} --否--> e[结束]
t --是--> a[向任务队列中添加任务] --> e
end
const callbacks = [];
let pending = false; // 用来标记是否是本轮事件循环中第一次使用 nextTick
function flushCallbacks(){
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0; // 清空 callbacks
// 依次执行 callbacks 中注册的回调
for(let i = 0; i < copies.length; i++){
copies[i]()
}
}
let microTimerFunc
const p = Promise.resolve()
// 优先使用微任务来实现异步
microTimerFunc = () => {
p.then(flushCallbacks)
}
export function nextTick(cb, ctx){
// 添加回调
callbacks.push(()=>{
if(cb) {
cb.call(ctx)
}
})
if (!pending) {
pending = true;
microTimerFunc()
}
}
上面的代码,我们演示了使用微任务注册回调的方式。但是在某些终端上,使用微任务会有些问题。所以,Vue.js 提供了在特殊场合下降级成宏任务的方式。
优先级如下:
Promise.then > setImmediate > MessageChannel > setTimeout
根据终端支持的情况,优先使用左侧的。
总结
简单总结下,nextTick 是将回调注册到一个异步队列中。同样, DOM 的更新也是使用的异步队列。nextTick 默认使用微任务,特殊场合下会降级成宏任务。