06_实现effect的stop和onStop功能
一、实现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);runner();expect(dummy).toBe(3);
});
通过以上单测,可以很明显地看出来,可以通过stop
函数传入runner
去停止数据的响应式,而当重新手动执行runner
的时候,数据又会恢复响应式。
(二)代码实现
从单测继续分析代码实现,通过stop
函数传入runner
,那就得继续回到effect.ts
,首先导出一个stop
函数。
export function stop(runner) {
}
再开始完善stop
函数。
继续分析:
通过runner
停止当前effect
的响应式 → 也就是从收集到当前effect
的dep
中将其删除,实际上是对effect
的操作,所以继续在ReactiveEffect
上维护一个stop
方法。
class ReactiveEffect {private _fn: any;// 在构造函数的参数上使用public等同于创建了同名的成员变量constructor(fn, public scheduler?) {this._fn = fn;}run() {activeEffect = this;return this._fn();}stop() {}
}
大致思路明白了,接下来解决第一个问题:如何通过runner
找到ReactiveEffect
的实例,然后去调用stop
。
答:在function effect() {}
中将_effect
挂载到runner
上。
所以需要改写一下之前的代码:
export function effect(fn, options: any = {}) {const _effect = new ReactiveEffect(fn, options.scheduler);_effect.run();const runner: any = _effect.run.bind(_effect);runner.effect = _effect;return runner;
}
那么我们导出的stop
函数的逻辑就清晰了。
export function stop(runner) {runner.effect.stop();
}
再来完善ReactiveEffect
类的stop
函数,也就是解决第二个问题:如何从收集到当前effect
的dep
中将其删除?
答:此时,我们并不知道当前effect
存在于哪些dep
中,所以考虑从track
时入手,在dep
收集activeEffect
后,让activeEffect
反向收集dep
,这样,就知道了当前effect
所在的dep
,接下来删掉就行了。
dep.add(activeEffect);
activeEffect.deps.push(dep);
class ReactiveEffect {private _fn: any;deps = [];// 在构造函数的参数上使用public等同于创建了同名的成员变量constructor(fn, public scheduler?) {this._fn = fn;}run() {activeEffect = this;return this._fn();}stop() {this.deps.forEach((dep: any) => {dep.delete(this);});}
}
功能完成后,继续看一下单测结果。
(三)代码优化
在完成功能以后,重新考虑对之前代码的实现。
1.代码可读性的问题抽离将当前依赖从收集到的dep中删除的逻辑,命名为cleanupEffect
,然后在类ReactiveEffect
的stop
中,直接调用cleanupEffect(this)
即可。function cleanupEffect(effect: any) {effect.deps.forEach((dep: any) => {dep.delete(effect);});}
2.性能问题当多次调用stop
时,实际上第一次已经删除了,后续调用都没有实际意义,只会引起无意义的性能浪费。 所以考虑给其一个active
状态,当被cleanupEffect
后,置为false
,不再进行再次删除。class ReactiveEffect {private _fn: any;deps = [];active = true;// 在构造函数的参数上使用public等同于创建了同名的成员变量constructor(fn, public scheduler?) {this._fn = fn;}run() {activeEffect = this;return this._fn();}stop() {// 要从收集到当前依赖的dep中删除当前依赖activeEffect// 但是我们根本不知道activeEffect存在于哪些dep中,所以就要用activeEffect反向收集depif (this.active) {cleanupEffect(this);this.active = false;}}}
* * *
二、实现onStop
(一)单元测试
it('onStop', () => {const obj = reactive({ prop: 1 });const onStop = jest.fn();let dummy;const runner = effect(() => {dummy = obj.foo;},{onStop,},);stop(runner);expect(onStop).toBeCalledTimes(1);
});
其实通过单测,可以看出功能跟stop
有些类似,逻辑也很简单,就是通过effect
的第二个参数,给定一个onStop
函数,当有这个函数时,我们再去调用stop(runner)
时,onStop
就会被调用一次。
那么实现思路也就很清晰了,我们首先得在ReactiveEffect
类中去接收这个函数,然后调用stop
的时候,手动调用一下onStop
即可。
(二)代码实现:
class ReactiveEffect {private _fn: any;deps = [];active = true;// + 定义函数可选onStop?: () => void;// 在构造函数的参数上使用public等同于创建了同名的成员变量constructor(fn, public scheduler?) {this._fn = fn;}run() {activeEffect = this;return this._fn();}stop() {// 要从收集到当前依赖的dep中删除当前依赖activeEffect// 但是我们根本不知道activeEffect存在于哪些dep中,所以就要用activeEffect反向收集depif (this.active) {cleanupEffect(this);// + 如果onStop有,就调用一次if (this.onStop) {this.onStop();}this.active = false;}}
}
export function effect(fn, options: any = {}) {const _effect = new ReactiveEffect(fn, options.scheduler);// + 接收onStop_effect.onStop = options.onStop;_effect.run();const runner: any = _effect.run.bind(_effect);runner.effect = _effect;return runner;
}
单测通过:
(三)代码优化
考虑到后续options
可能还会传入很多其他选项,所以进行一下重构
Object.assign(_effect, options);
感觉语义化稍弱,所以,就抽离出一个extend
方法,又考虑到这个方法可以抽离成一个工具函数
,所以在src
下建立shared
目录,然后建立index.ts
,专门放置各个模块通用的工具函数。
// src/shared/index.ts
export const extend = Object.assign;
extend(_effect, options);
当然,重构完以后,别忘了重新跑一下effect
单测。
(四)解决问题的思路
可以看到effect
的单测是通过的,那完成这一组功能后,继续完成的跑一下所有单测,看看是否对其他功能造成影响。
yarn test
果然,不出意外的话,出现意外了。
可以看到是reactive
的happy path
单测出了问题,而且activeEffect
是个undefined
,那我们回去重新看一下。
不难看出observed.foo
也是触发了get
操作,也就是触发了track
去收集依赖,而此时并没有effect
包裹着的依赖存在,所以run
不会执行,也就没有activeEffect
,所以此时我们并不应该去收集依赖,所以增加一个判断。
if (!activeEffect) return;
dep.add(activeEffect);
activeEffect.deps.push(dep);
为了验证结果,再次跑一下全部的单测。
最后
最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。
有需要的小伙伴,可以点击下方卡片领取,无偿分享