Skip to content

什么是调度器

调度器(scheduler)用于自定义 effect 的执行时机与频率,让副作用的触发更可控、更高效。

ts

const scheduler = job => {
  // 将 effect 的真正执行放到微任务中
  Promise.resolve().then(job)
}

effect(() => {
  console.log(count.value)
}, {scheduler})

// 当 count.value 变化时,effect 不会“立刻”执行,而是交给调度器异步调度
count.value++

为什么需要调度器

在默认实现中,effect 会在依赖数据变化时立即重新执行。 当变更“密集且连续”时,会产生大量重复计算,带来不必要的性能开销。

例如,在动画循环中多次更新同一响应式数据,每次更新都会触发 effect 重新执行,容易造成性能下降:

js
import {ref, effect} from '../dist/reactivity.esm.js'

const count = ref(0)
effect(() => {
  console.log('count.value ==> ', count.value)
})

function animate() {
  for (let i = 0; i < 100; i++) {
    count.value = i
  }
  requestAnimationFrame(animate)
}

requestAnimationFrame(animate)

在这个例子中,count.value 在动画循环中被频繁更新;若没有调度器进行“合并/节流”,每次赋值都会触发一次 effect 执行,从而放大性能问题。

基础用法

ts
const count = ref(0)

effect(() => {
  console.log('count.value ==> ', count.value)
}, {
  scheduler: () => {
    console.log('调度器被调用')
  }
})

setTimeout(() => {
  count.value = 1
}, 1000)

当前输出

js
// 0 
// 1s 后
// 1

期望输出

js
// 0 
// 1s 后
// 调度器被调用

实现

在实现前,先回顾一个 Class 的小特性: 当实例与原型存在同名方法时,调用会优先命中实例方法。

js
class Person {
  constructor(name) {
    this.name = name
  }

  sayHi() {
    console.log('原型方法', this.name)
  }
}

const p = new Person('张三')
p.sayHi() // 原型方法 张三
js
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() // 实例方法 张三

有了这个特性,我们就可以很自然地实现“可选调度”:

  • 如果传入了 scheduler,则以用户的调度逻辑为准;
  • 如果未传入,则回退到默认行为(直接执行 run)。

实现思路:

  1. scheduler 是可选的。传入时,借助“实例属性覆盖原型方法”的特性,将其挂到 ReactiveEffect 实例上;
  2. 为统一触发入口,新增 notify 方法,由它来调度执行(调用实例上的 scheduler)。
ts
export let activeSub: ReactiveEffect | undefined

export class ReactiveEffect {
  constructor(public fn: Function) {
  }

  run(): any {
    const prevSub = activeSub // 将当前的 effect 保存下来, 防止嵌套时丢失

    activeSub = this

    try {
      return this.fn()
    } finally {
      // 执行完成的时候, 恢复之前的 effect
      activeSub = prevSub
    }
  }

  /**
   * 依赖数据发生变化时的统一触发入口
   */
  notify() {
    this.scheduler()
  }

  /**
   * 默认调度:调用 run;若用户在实例上提供了 scheduler,将覆盖本方法
   */
  scheduler() {
    this.run()
  }
}

export function effect(fn, options = {}) {
  const e = new ReactiveEffect(fn)
  Object.assign(e, options)
  e.run()

  const runner = e.run.bind(e)
  runner.effect = e

  return runner
}

相应地,system.ts 也需要配合修改:

ts
export function propagate(subs: Link) {
  // ...省略无关代码
  queuedEffect.forEach(effect => effect.notify())
}

最后更新时间: