scheduler
这里引用一下 Vue 官方的经典测试用例来测试 scheduler 功能
it("scheduler", () => { let dummy; let run: any; const scheduler = jest.fn(() => { run = runner; }); const obj = reactive({ foo: 1 }); const runner = effect( () => { dummy = obj.foo; }, { scheduler } ); expect(scheduler).not.toHaveBeenCalled(); expect(dummy).toBe(1); // should be called on first trigger obj.foo++; expect(scheduler).toHaveBeenCalledTimes(1); // // should not run yet expect(dummy).toBe(1); // // manually run run(); // // should have run expect(dummy).toBe(2); }); 复制代码
测试用例的执行流程大致为:声明一个 scheduler,使用 reactive 声明一个对象 obj,使用 effect 对 obj.foo 进行依赖收集;此时断言传入的 scheduler 没有被执行,但是 fn 执行,所以 dummy 的值为 1;然后对 obj.foo 加一,此时应该调用一次 scheduler,scheduler 函数内部并没有对 dummy 的赋值, 所以 dummy 此时还是 1;然后执行 runner,对 dummy 进行赋值,此时 dummy 值为 2。
scheduler的作用是:当传入scheduler后,target修改的时候,trigger的时候绕过fn,直接执行传入的scheduler。
这就需要在 effect 里判断是否有 scheduler,如果有则执行 scheduler,否则执行 fn
根据这个思路我们来修改我们之前的 effect 函数
class ReactiveEffect { private _fn: Function public scheduler?: Function constructor(fn: Function, scheduler?: Function) { this._fn = fn this.scheduler = scheduler } run() { activeEffect = this return this._fn() } } let activeEffect: ReactiveEffect; export function effect(fn: Function, options: any = {}) { const _effect = new ReactiveEffect(fn, options.scheduler) _effect.run() return _effect.run.bind(_effect) } 复制代码
然后我们需要在 trigger 函数中去判断执行 scheduler 还是 fn
export function trigger<T extends object>(target: T, key: keyof T) { let depsMap = targetMap.get(target) let dep = depsMap!.get(key) as Set<ReactiveEffect> for (const effect of dep) { if (effect.scheduler) effect.scheduler() else effect.run() } } 复制代码
执行测试
stop
同样的我们还是通过测试用例来完成 stop 功能的实现
it("stop", () => { let dummy; const obj = reactive({ prop: 1 }); const runner = effect(() => { dummy = obj.prop; }); obj.prop = 2; expect(dummy).toBe(2); stop(runner); obj.prop = 3; expect(dummy).toBe(2); // stopped effect should still be manually callable runner(); expect(dummy).toBe(3); }); 复制代码
测试用例执行流程:使用 reactive 声明一个对象 obj,使用 effect 为 obj.prop 添加依赖(将 obj.prop的值赋给 dummy),然后给 obj.prop 赋值,此时 dummy 的值应该为 2;然后使用 stop 函数取消 obj.prop 的这个依赖,再给 obj.prop 加 1,此时 dummy 不会再被赋值,值仍然为 2;然后重新执行 runner,此时 dummy 又被重新赋值为 obj.prop。
然后我们来编写 stop 方法,它仍然是 effect 文件导出的一个模块
export function stop(runner: any) { runner.effect.stop() } 复制代码
这个 runner 就是我们之前的 run 方法的包装,我们可以将之前的 effect 绑定在runner 函数之上
export function effect(fn: Function, options: any = {}) { const _effect = new ReactiveEffect(fn, options.scheduler) _effect.run() const runner: any = _effect.run.bind(_effect) runner.effect = _effect return runner } 复制代码
然后我们在 ReactiveEffect 上声明 stop 方法和 deps 数组
class ReactiveEffect { private _fn: Function public scheduler?: Function deps: Set<ReactiveEffect>[] = [] constructor(fn: Function, scheduler?: Function) { this._fn = fn this.scheduler = scheduler } run() { activeEffect = this return this._fn() } stop() { this.deps.forEach(dep => { dep.delete(this) }) } } 复制代码
其次,需要在收集依赖的时候同时将依赖保存到 effect 上
export function track<T extends object>(target: T, key: keyof T) { // target -> key -> dep let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, depsMap = new Map()) } let dep = depsMap.get(key) if (!dep) { depsMap.set(key, dep = new Set<ReactiveEffect>()) } // 如果没有 activeEffect 不执行依赖收集 if (!activeEffect) return; // 收集依赖 dep.add(activeEffect) // 反向收集依赖 activeEffect.deps.push(dep) } 复制代码
此时执行测试
可以看到已经完成了测试用例,但是,这时候代码还不够完善,有很大的优化空间。
为了代码的可读性,我们将 stop 的逻辑抽离出来作为一个函数
// 清除依赖 function cleanupEffect(effect: ReactiveEffect) { effect.deps.forEach(dep => { dep.delete(effect) }) } 复制代码
此外还有一个性能问题,每次当我们调用 stop 的时候,都会遍历清除,但是,只需要清除一次之后就不需要再清除了,所以我么你可以通过提供一个状态来控制 stop 的次数
stop() { if (this.active) { cleanupEffect(this) this.active = false } } 复制代码
再次执行测试,测试仍然成功。
然后,与 stop 相关的还有一个 onStop 事件,我们还是通过测试用例来入手
it("events: onStop", () => { const onStop = jest.fn(); const runner = effect(() => {}, { onStop, }); stop(runner); expect(onStop).toHaveBeenCalled(); }); 复制代码
onStop 是一个回调函数,当触发 stop 时会被调用
export function effect(fn: Function, options: any = {}) { const _effect = new ReactiveEffect(fn, options.scheduler) _effect.onStop = options.onStop _effect.run() const runner: any = _effect.run.bind(_effect) runner.effect = _effect return runner } 复制代码
然后在 ReactiveEffect 内声明 onStop,在调用 cleanupEffect 之后调用
stop() { if (this.active) { cleanupEffect(this) if (this.onStop) { this.onStop() } this.active = false } } 复制代码
执行测试,通过
这里的 onStop 赋值可以优化一下,后面还有会很多属性复制,所以我们直接将这里的赋值逻辑优化一下
// /src/shared/index.ts export const extend = Object.assign // /src/reactivity/effect.ts export function effect(fn: Function, options: any = {}) { const _effect = new ReactiveEffect(fn, options.scheduler) // 将 options 的内容合并到 effet 上 extend(_effect, options) _effect.run() const runner: any = _effect.run.bind(_effect) runner.effect = _effect return runner } 复制代码
虽然上面的测试用例通过了,但是,如果将测试用例中的 obj.prop = 3
改为obj.prop++
再执行测试用例会发现测试失败。
(下面的代码在 isReactive 之后完善,所以会有较大变化, 可以看完 isReactive 之后再看这里)
这是因为,obj.prop++
的操作是一个 get + set 的操作,完整的表达式应该是obj.prop = obj.prop + 1
,在 get 的过程中会重新触发依赖收集,所以导致上面的 stop 失效。
解决这个问题可以通过再声明一个全局变量来控制,当变量为 false 时停止收集依赖
// global let shouldTrack: boolean; // class ReactiveEffect run() { if (!this.active) // 如果被 stop 直接执行返回 return this._fn() // 否则进行依赖收集 shouldTrack = true activeEffect = this const result = this._fn() shouldTrack = false // 依赖收集结束之后将状态重置 return result } // track export function track<T extends object>(target: T, key: keyof T) { // target -> key -> dep let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, depsMap = new Map()) } let dep = depsMap.get(key) if (!dep) { depsMap.set(key, dep = new Set<ReactiveEffect>()) } // 如果没有激活的 effect 中断执行 if (!activeEffect) return; // 如果不应该收集依赖, 中断执行 if (!shouldTrack) return // 收集依赖 dep.add(activeEffect) // 反向收集依赖 activeEffect.deps.push(dep) } 复制代码
此时执行测试,测试通过