自我介绍:大家好,我是吉帅振的网络日志;微信公众号:吉帅振的网络日志;前端开发工程师,工作4年,去过上海、北京,经历创业公司,进过大厂,现在郑州敲代码。
一、前言
回调函数是以一种调度的方式执行的,特别是当 flush 不是 sync 时,它会把回调函数执行的任务推到一个异步队列中执行。接下来,我们就来分析异步执行队列的设计。
二。异步任务队列的设计
我们把之前的例子简单修改一下:
import { reactive, watch } from 'vue'
const state = reactive({ count: 0 })
watch(() => state.count, (count, prevCount) => {
console.log(count)
})
state.count++
state.count++
state.count++
这里,我们修改了三次 state.count,那么 watcher 的回调函数会执行三次吗?答案是不会,实际上只输出了一次 count 的值,也就是最终计算的值 3。这在大多数场景下都是符合预期的,因为在一个 Tick(宏任务执行的生命周期)内,即使多次修改侦听的值,它的回调函数也只执行一次。组件的更新过程是异步的,我们知道修改模板中引用的响应式对象的值时,会触发组件的重新渲染,但是在一个 Tick 内,即使你多次修改多个响应式对象的值,组件的重新渲染也只执行一次。这是因为如果每次更新数据都触发组件重新渲染,那么重新渲染的次数和代价都太高了。
那么,这是怎么做到的呢?
1.异步任务队列的创建
通过前面的分析我们知道,在创建一个 watcher 时,如果配置 flush 为 pre 或不配置 flush ,那么 watcher 的回调函数就会异步执行。此时分别是通过 queueJob 和 queuePostRenderEffect 把回调函数推入异步队列中的。在不涉及 suspense 的情况下,queuePostRenderEffect 相当于 queuePostFlushCb,我们来看它们的实现:
// 异步任务队列
const queue = []
// 队列任务执行完后执行的回调函数队列
const postFlushCbs = []
function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job)
queueFlush()
}
}
function queuePostFlushCb(cb) {
if (!isArray(cb)) {
postFlushCbs.push(cb)
}
else {
// 如果是数组,把它拍平成一维
postFlushCbs.push(...cb)
}
queueFlush()
}
Vue.js 内部维护了一个 queue 数组和一个 postFlushCbs 数组,其中 queue 数组用作异步任务队列, postFlushCbs 数组用作异步任务队列执行完毕后的回调函数队列。执行 queueJob 时会把这个任务 job 添加到 queue 的队尾,而执行 queuePostFlushCb 时,会把这个 cb 回调函数添加到 postFlushCbs 的队尾。它们在添加完毕后都执行了 queueFlush 函数,我们接着看它的实现:
const p = Promise.resolve()
// 异步任务队列是否正在执行
let isFlushing = false
// 异步任务队列是否等待执行
let isFlushPending = false
function nextTick(fn) {
return fn ? p.then(fn) : p
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
nextTick(flushJobs)
}
}
可以看到,Vue.js 内部还维护了 isFlu