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的相关知识,提升工作效率。




有需要的小伙伴,可以点击下方卡片领取,无偿分享



















