Appearance
什么是调度器
调度器(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
)。
实现思路:
scheduler
是可选的。传入时,借助“实例属性覆盖原型方法”的特性,将其挂到ReactiveEffect
实例上;- 为统一触发入口,新增
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())
}