stop
单测
// effect.spec.ts
describe('effect', () => {
...
it("stop", () => {
let dummy;
const obj = reactive({ prop: 1 });
const runner = effect(() => {
dummy = obj.prop;
});
// 执行 reactive 的 set
obj.prop = 2;
// 触发依赖
expect(dummy).toBe(2);
// 执行 stop 方法
stop(runner);
// 执行 reactive 的 set
obj.prop = 3;
// 依赖没有如预想中的那样被触发
expect(dummy).toBe(2);
// 重新执行 runner
runner();
// 依赖被触发
expect(dummy).toBe(3);
})
})
从上述单元测试可知,stop 的作用是当 reactive 对象在执行 set 时候 阻止触发依赖,那么如何阻止呢?
先来看下之前实现的触发依赖代码
// effect.ts
export function trigger(target, key) {
...
for(let effect of dep) {
if(effect.scheduler) {
effect.scheduler();
} else {
effect.run()
}
}
}
回顾之前trigger的代码可以知道通过遍历存储依赖的容器 dep, 依次执行 effect 的 run 方法来实现触发依赖,那么我们只要把 effect 从 dep 中删除,就可以实现 stop 的功能了
实现代码
// effect.ts
class ReactiveEffect {
private _fn;
public scheduler: Function | undefined;
deps: [];
active = true;
constructor(fn, scheduler?) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
activeEffect = this;
return this._fn();
}
stop() {
// 避免多次调用 stop 执行 cleanupEffect 方法
if(this.active) {
cleanupEffect(this);
this.active = false;
}
}
}
function cleanupEffect(effect) {
effect.deps.forEach((dep: any) => {
dep.delete(effect);
})
}
export function track(target, key) {
...
// 当单纯的 reactive 获取触发 track 的时候是没有 activeEffect 的,所以要判断一下
if (activeEffect) {
dep.add(activeEffect);
// effect 反向收集 deps
activeEffect.deps.push(dep);
}
}
let activeEffect;
export function effect(fn, options: any = {}) {
const __effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
const runner: any = _effect.run.bind(_effect);
// 把 effect 实例绑定在 runner 上
runner.effect = _effect;
return runner;
}
export function stop(runner) {
runner.effect.stop();
}
onStop
单测
// effect.spec.ts
describe('effect', () => {
it("onStop", () => {
const obj = reactive(() => {
foo: 1;
});
const onStop = jest.fn();
let dummy;
const runner = effect(
() => {
dummy = obj.foo;
},
{ onStop }
);
stop(runner);
expect(onStop).toBeCalledTimes(1);
});
})
从上述单元测试可知,当执行 stop 的时候, 如果 effect 存在 onStop 的话,那么 onStop 会被执行
实现代码
// shared index.ts
export const extend = Object.assign;
// effect.ts
class ReactiveEffect {
private _fn;
public scheduler: Function | undefined;
deps: [];
active: true;
onStop?: () => void;
constructor(fn, scheduler?) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
...
}
stop() {
if(this.active) {
cleanupEffect(this);
// options 中存在 onStop 则执行 onStop
if(this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
export function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
extend(_effect, options);
...
}
stop优化
当单元测试是这样的时候会无法通过
// effect.spec.ts
describe("effect", () => {
...
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;
obj.prop++;
expect(dummy).toBe(2); // 报错
runner();
expect(dummy).toBe(3);
});
})
思考:obj.foo++ 发生了什么,导致上面写的 stop 方法 失效
- obj.foo++ 触发了 reactive 的 get set 操作,导致原先删除的依赖重新收集了一遍,然后触发依赖
解决方法: 通过一个变量 shouldTrack 控制依赖的收集
思考:shouldTrack 应该在什么时候被赋值
- 当调用 set 的时候,触发了 trigger 重新执行 fn,继而触发了响应式对象的 get 操作,因此应该在 run() 中做相对应的处理
// effect.ts
let activeEffect;
let shouldTrack;
class ReactiveEffect {
private _fn: any;
public scheduler: Function | undefined;
deps = [];
active = true;
onStop?: () => void;
constructor(fn, scheduler?: Function) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
// stop 方法执行后走这里
if (!this.active) {
return this._fn();
}
shouldTrack = true;
activeEffect = this;
const result = this._fn();
shouldTrack = false;
return result;
}
stop() {
...
}
}
let targetMap = new Map();
export function track(target, key) {
// shouldTrack 为 false 或 activeEffect 不存在
if (!isTracking()) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
// 依赖已经存在 dep 中
if (dep.has(activeEffect)) return;
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
function isTracking() {
return shouldTrack && activeEffect !== undefined;
}