Vue3 的响应式系统源码级 effect 的 scheduler 选项与批量更新调度优化原理
字数 1071 2025-12-12 15:37:05
Vue3 的响应式系统源码级 effect 的 scheduler 选项与批量更新调度优化原理
一、知识描述
scheduler 是 Vue3 响应式系统中 effect 函数的一个关键配置选项,它允许开发者自定义副作用函数的调度执行时机。这个机制是实现异步批处理更新、计算属性延迟计算、watch 回调调度等高级特性的核心。通过 scheduler,Vue3 可以将多个同步触发的更新任务收集起来,在下一个事件循环中批量执行,从而避免不必要的重复计算和渲染,显著提升性能。
二、循序渐进讲解
1. 前置知识回顾
effect:副作用函数,是 Vue3 响应式系统的基本单元。当响应式数据变化时,与之关联的effect会被重新执行。- 依赖收集:通过
track函数,在effect执行时,将当前effect记录到响应式数据的依赖映射中。 - 触发更新:通过
trigger函数,在响应式数据变化时,取出所有关联的effect并执行。
2. 没有 scheduler 的基本流程
// 简化的 effect 执行逻辑(无 scheduler)
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(effect => {
effect() // 直接同步执行副作用函数
})
}
问题:如果同一个响应式数据在同一个同步任务中被多次修改,关联的 effect 会被同步触发多次,造成性能浪费。
3. scheduler 的引入与基本结构
effect 函数接受一个选项对象,其中包含 scheduler 属性:
effect(
() => { console.log('副作用执行') },
{
scheduler(effect) { // effect 是副作用函数本身
// 自定义调度逻辑
queueJob(effect) // 典型的入队操作
}
}
)
在 trigger 函数中,优先执行 scheduler:
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effect => {
// 如果 effect 存在 scheduler,则执行 scheduler 而不是直接运行 effect
if (effect.scheduler) {
effect.scheduler(effect) // 将调度权交给 scheduler
} else {
effectsToRun.add(effect)
}
})
effectsToRun.forEach(effect => effect()) // 没有 scheduler 的才直接执行
}
4. Vue3 的队列实现(queueJob 与 queueFlush)
Vue3 维护了一个队列来管理需要调度的任务:
const queue = [] // 任务队列
let isFlushing = false // 是否正在刷新队列
let isFlushPending = false // 是否等待刷新
const resolvedPromise = Promise.resolve() // 用于创建微任务
function queueJob(job) {
// 去重:同一个 job 只添加一次
if (!queue.includes(job)) {
queue.push(job)
}
// 触发队列刷新
queueFlush()
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
// 使用微任务延迟执行
resolvedPromise.then(flushJobs)
}
}
5. 批量执行与任务排序(flushJobs)
function flushJobs() {
isFlushPending = false
isFlushing = true
// 重要:任务排序,确保:
// 1. 父组件的更新优先于子组件(因为父组件id较小)
// 2. 用户自定义的 watch 回调在组件更新后执行
// 3. 如果一个组件在父组件更新时被卸载,它的更新可以被跳过
queue.sort((a, b) => a.id - b.id)
for (let i = 0; i < queue.length; i++) {
const job = queue[i]
job() // 执行任务
}
queue.length = 0 // 清空队列
isFlushing = false
// 注意:在 flushJobs 执行过程中,可能又有新任务被加入队列
// 如果此时队列不为空,需要再次刷新
if (queue.length) {
queueFlush()
}
}
6. 在组件更新中的具体应用
在组件渲染的 setupRenderEffect 中:
function setupRenderEffect(instance, vnode, container) {
instance.update = effect(() => {
// 组件渲染逻辑
const subTree = render.call(instance.proxy)
patch(prevTree, subTree, container)
}, {
scheduler: () => {
// 将组件更新任务加入队列
queueJob(instance.update)
}
})
}
这样,当多个响应式数据变化触发同一个组件更新时,更新函数只会被加入队列一次。
7. watch 和 computed 中的调度应用
- watch:默认使用
scheduler将回调推迟到组件更新之后执行 - computed:通过
scheduler实现惰性重新计算,只有在真正读取 value 时才重新计算
8. 性能优势总结
- 去重优化:同一个 effect 在单次事件循环中只执行一次
- 批量更新:多个数据变化触发的更新合并为一次渲染
- 执行顺序控制:确保父组件先于子组件更新
- 异步执行:避免同步执行导致的中间状态渲染
三、实际例子
const state = reactive({ count: 0, name: 'vue' })
effect(() => {
console.log('渲染:', state.count, state.name)
}, {
scheduler(effect) {
console.log('调度任务')
setTimeout(effect, 0) // 可以改为宏任务执行
}
})
// 同步修改两次
state.count++
state.name = 'vue3'
// 输出:"调度任务" 只打印一次
// 然后(下一个事件循环)输出:"渲染: 1 vue3"
这个机制确保了无论同步修改多少次响应式数据,副作用函数在单个事件循环中只执行一次。