2025.09.06
调度策略:Scheduler 优化更新性能
Scheduler 调度器:优化响应式更新的执行策略
🎯 调度器的本质与作用
调度器(Scheduler)是响应式系统中的 “执行策略制定者” 。它不再让 effect 在数据变化时立即重新执行,而是允许开发者接管和控制
effect 的执行时机、频率和优先级。
核心作用: 将副作用的触发(trigger)与副作用的执行(run)进行解耦。
function scheduler(job: () => void) {
// 将 effect 的真正执行放入微任务队列
Promise.resolve().then(job) // 延迟执行
}
effect(() => {
// ... 副作用逻辑
}, { scheduler }) // 传入自定义调度策略
为什么响应式系统需要调度器
在默认(无调度器)的实现中,数据变化会同步触发 effect。当数据更新密集且连续时,系统会产生巨大的性能开销。
性能浪费示例:密集计算
考虑在一次动画循环(单帧)中,同一个响应式数据被连续更新 100 次:
// ...
const count = ref(0)
function animate() {
for (let i = 0; i < 100; i++) {
count.value = i // 💥 默认行为下,effect 会立即执行 100 次
}
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
用户只需要看到最终 count 值为 99 时的 UI 结果,中间的 99 次重复计算和 DOM 操作完全是浪费。调度器正是用于解决这种
"雪崩式" 的重复计算问题,通过 批处理 或 节流 来确保 effect 只执行一次。
调度器的基础用法与行为对比
调度器通过 effect 函数的 options 参数传入。
const count = ref(0)
effect(() => {
console.log('count.value ==> ', count.value)
}, {
// 自定义调度策略:不立即执行 run()
scheduler: () => {
console.log('📢 调度器被调用,等待执行...')
}
})
// ...
count.value = 1 // 触发更新
| 场景 | 触发时间 | 执行函数 | 输出结果 |
|---|---|---|---|
| 初始执行 | 立即 | effect -> run() | count.value ==> 0 |
| 无调度器 | 同步立即执行 | trigger -> run() | count.value ==> 1 |
| 有调度器更新 | 异步(由 setter 调用) | trigger -> scheduler() | 📢 调度器被调用,等待执行... |
调度器的实现原理
JavaScript 类的方法覆盖特性
在实现调度器前,我们先回顾一个 JavaScript 类的重要特性:当实例与原型存在同名方法时,调用会优先使用实例方法。
原型方法示例:
class Person {
constructor(name) {
this.name = name
}
sayHi() {
console.log('原型方法', this.name)
}
}
const p = new Person('张三')
p.sayHi() // 输出:原型方法 张三
实例方法覆盖示例:
class Person {
constructor(name) {
this.name = name
}
sayHi() {
console.log('原型方法', this.name)
}
}
const p = new Person('张三')
// 实例方法覆盖原型方法
p.sayHi = function () {
console.log('实例方法', this.name)
}
p.sayHi() // 输出:实例方法 张三
⚙️ 调度器的优雅实现
我们利用 js 实例方法优先于原型方法的特性,在 ReactiveEffect 类上实现一个灵活的调度机制。
关键设计: notify 和 scheduler 方法
- 默认入口
notify(): 作为数据变化后的唯一入口, 职责是通知effect需要更新 - 默认
scheduler()(原型): 在原型上定义, 默认行为是直接调用this.run() - 用户
scheduler(实例): 用户传入的自定义调度函数通过Object.assign挂载到实例上, 覆盖原型方法
🛠️ ReactiveEffect 类结构改进
export class ReactiveEffect {
// ... constructor, run() 方法 (已包含上下文保存/恢复逻辑)
/**
* 1. 📢 统一触发入口:当数据变化时,propagete 调用此方法
*/
notify() {
this.scheduler() // 统一转发到 scheduler
}
/**
* 2. 默认调度策略 (原型方法)
* 如果实例上没有 scheduler,则执行此方法
*/
scheduler() {
this.run() // 默认:同步执行副作用
}
}
export function effect(fn, options = {}) {
const e = new ReactiveEffect(fn)
// 3. 实例方法覆盖:将用户提供的 scheduler 挂载到实例 e
Object.assign(e, options)
e.run() // 首次执行
// ... 返回 runner
}
🔗 system.ts 配合修改
在底层依赖触发模块 system.ts 中,我们不再直接调用 effect.run(),而是调用 effect.notify()
// system.ts -> propagate 函数片段
export function propagate(subs: Link) {
// ...
// 核心变更:通过 notify 间接调用 scheduler/run
queuedEffect.forEach(effect => effect.notify())
}
🎉 重构成果总结
通过引入 notify 和 scheduler 方法,我们成功解耦了响应式系统的两大核心环节:
| 机制 | 职责 | 实现位置 |
|---|---|---|
触发(Trigger) | 数据变化时,通知依赖集合。 | setter -> trigger -> propagate |
通知(Notify) | 接收到触发信号,决定下一步动作。 | ReactiveEffect.notify() |
调度(Scheduler) | 执行策略,决定 run 的时机和频率。 | ReactiveEffect.scheduler() |
这种设计极大地提高了系统的灵活性,并为 Vue 框架实现异步更新、批量处理 DOM 操作提供了核心机制。