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 类上实现一个灵活的调度机制。

关键设计: notifyscheduler 方法

  1. 默认入口notify(): 作为数据变化后的唯一入口, 职责是通知 effect 需要更新
  2. 默认 scheduler()(原型): 在原型上定义, 默认行为是直接调用 this.run()
  3. 用户 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())
}

🎉 重构成果总结

通过引入 notifyscheduler 方法,我们成功解耦了响应式系统的两大核心环节:

机制职责实现位置
触发(Trigger)数据变化时,通知依赖集合。setter -> trigger -> propagate
通知(Notify)接收到触发信号,决定下一步动作。ReactiveEffect.notify()
调度(Scheduler)执行策略,决定 run 的时机和频率。ReactiveEffect.scheduler()

这种设计极大地提高了系统的灵活性,并为 Vue 框架实现异步更新、批量处理 DOM 操作提供了核心机制。