在Vue
中源码中,用户使用的api在底层都会被解释为一个一个的任务,例如nextTick
、watcher
、computed
等等,我们传入的回调函数都会被放入到一个队列中,然后在合适的时机执行;
而怎么判断这个队列中是否有数据需要执行?队列执行完毕后怎么清空队列?新任务如何加入队列?
今天就来看一下Vue
的任务执行机制,来实现一个简单的任务队列,简化我们日常开发中的一些操作;
nextTick 怎么保证最后执行?
在我之前写的Vue
源码解析系列中,有一篇文章是讲Vue
的nextTick
的实现原理的,这里就不再赘述了,有兴趣的可以去看看:【源码&库】Vue3 中的 nextTick 魔法背后的原理
在Vue
中,例如nextTick
我们使用都是如下:
nextTick(() => {
console.log('nextTick')
})
在nextTick
的背后,我们的回调函数最后其实是类似下面这样执行的:
// 伪代码
let queue = Promise.resolve();
const nextTick = (cb) => {
queue = queue.then(cb);
};
而我们如果操作响应式数据的任务在Vue
的背后类似如下面这样执行的:
// 伪代码
const count = ref(0);
// 依赖收集
effect(() => {
console.log(count.value);
});
const effect = (cb) => {
// 依赖收集
queue.then(cb);
};
这样就可以保证nextTick
一定在effect
执行完毕之后执行,感兴趣的可以拿上面的代码尝试一下;
任务队列怎么实现?
上面的代码其实并没有任何队列的影子,只有一个Promise
,队列的实现全靠Promise
的特性,then
可以注册多个回调函数,并且会按照注册的顺序执行;
而上面的伪代码只是说明怎么保证nextTick
在最后执行,在Vue
中是有一个专门的队列来存放这些任务的,完整伪代码如下:
let p = Promise.resolve();
const queue = [];
// job 就理解为一个函数即可
const queueJob = (job) => {
queue.push(job);
queueFlush();
};
const queueFlush = () => {
p = p.then(flushJobs)
};
const flushJobs = () => {
try {
for (const job of queue) {
job();
}
} finally {
queue.length = 0;
}
}
const nextTick = (cb) => {
p = p.then(cb);
};
const effect = (cb) => {
// 依赖收集
queueJob(cb);
};
上面就是一个最小的Vue
的队列执行,我们可以看到,每次执行完队列中的任务后,都会清空队列,这样就保证了每次执行的任务都是最新的;
而Vue
中的任务队列是按JS
的宏任务为批次来执行的,整体逻辑非常清晰:
- 一个宏任务中执行的过程中,会出现很多
Vue
的任务,这些任务会被放入到一个队列中; - 当一个任务入队后,会立即执行队列执行的函数,然后执行队列刷新函数;
- 队列刷新函数是一个异步执行的函数,会在当前宏任务执行完毕后执行;
- 宏任务执行完毕,根据JS的事件循环机制,会执行微任务,这个时候会自动执行队列刷新函数;
- 队列刷新函数会执行队列中的所有任务,然后清空队列;
- 根据
Promise
的特性,then
可以注册多个回调函数,这个时候会按照注册的顺序执行回调函数; - 而
nextTick
的回调函数就是注册在Promise
的then
中的,所以nextTick
的回调函数会队列刷新函数执行完毕后执行;
到这里我直呼内行,这个队列执行机制真的是妙啊,而且代码量也不多,非常简洁;
总结
Vue
的nextTick
全靠Promise
的特性来实现,而Vue
的内部也是一个一个的队列任务批量执行,内部用到了不少的知识点:
Promise
的特性,这一点是我最惊艳的点,还可以这样用;JS
的事件循环机制,这个是必须要掌握的;- 队列数据结构,非常实用;
虽然被我简化之后代码很少,但是保留的核心思想,这个队列执行机制还是非常值得学习的,可以用到很多地方;
当然Vue
内部还是有很多的细节的,感兴趣的可以去看看源码,谢谢大家的阅读,欢迎评论交流;