一、单元测试编写
前面实现effectde stop功能中,对于以下obj.prop++的情况无法满足停止触发依赖。测试用例如下
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++ 可以拆分为obj.prop = obj.porp + 1,涉及到一个get和set
obj.prop++
expect(dummy).toBe(2);
// stopped effect should still be manually callable
runner();
expect(dummy).toBe(3);
});
以上测试会报错,原因是opj.prop++可以拆分为obj.prop = obj.prop + 1,涉及到一个get和set,之前的只涉及到set操作。而get操作会重新收集依赖。导致之前stop的清理依赖无效。
二、功能修改
我们可以在原来基础上新增一个shouldTrack来决定在执行tack的时候是否应该收集依赖。 原来实现如下:
import {extend} from '../shared'
class ReactiveEffect{
private _fn:any;
deps = [];
active = true;
onStop?:() => void;
constructor(fn, public scheduler?){
this._fn = fn;
}
run(){
activeEffect = this;
return this._fn();
}
stop(){
if(this.active){
cleanupEffect(this);
if(this.onStop){
this.onStop();
}
this.active = false;
}
cleanupEffect(this);
}
}
function cleanupEffect(effect){
effect.deps.forEach((dep: any) => {
dep.delete(effect);
})
}
const targetMap = new Map();
export function track(target,key){
// target -> key -> dep
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)
}
if(!activeEffect) return;
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
export function trigger(target,key){
let depsMap = targetMap.get(target);
let dep = depsMap.get(key);
for(const effect of dep){
if(effect.scheduler){
effect.scheduler();
} else {
effect.run();
}
}
}
let activeEffect;
export function effect(fn, options: any= {}){
const _effect = new ReactiveEffect(fn,options.scheduler);
// options
Object.assign(_effect,options);
// extend
extend(_effect,options);
_effect.onStop = options.onStop;
_effect.run();
const runner: any = _effect.run.bind(_effect);
runner.effect = _effect;
return runner
}
export function stop(runner){
runner.effect.stop()
}
修改后的代码如下:
import {extend} from '../shared'
//新增
let activeEffect;
//新增
let shouldTrack;
class ReactiveEffect{
private _fn:any;
deps = [];
active = true;
onStop?:() => void;
constructor(fn, public scheduler?){
this._fn = fn;
}
run(){
if(!this.active){
return this._fn()
}
shouldTrack = true;
activeEffect = this;
const result = this._fn()
// reset
shouldTrack = false;
return result;
}
stop(){
if(this.active){
cleanupEffect(this);
if(this.onStop){
this.onStop();
}
this.active = false;
}
cleanupEffect(this);
}
}
function cleanupEffect(effect){
effect.deps.forEach((dep: any) => {
dep.delete(effect);
})
effect.deps.length = 0;
}
const targetMap = new Map();
export function track(target,key){
if(!isTracking()) return;
// target -> key -> dep
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)
}
if(dep.has(activeEffect)) return;
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
//新增
function isTracking () {
return shouldTrack && activeEffect!== undefined;
}
export function trigger(target,key){
let depsMap = targetMap.get(target);
let dep = depsMap.get(key);
for(const effect of dep){
if(effect.scheduler){
effect.scheduler();
} else {
effect.run();
}
}
}
export function effect(fn, options: any= {}){
const _effect = new ReactiveEffect(fn,options.scheduler);
// options
Object.assign(_effect,options);
// extend
extend(_effect,options);
_effect.onStop = options.onStop;
_effect.run();
const runner: any = _effect.run.bind(_effect);
runner.effect = _effect;
return runner
}
export function stop(runner){
runner.effect.stop()
}
三、执行测试
完成后执行单元测试,发现对于obj.prop++的测试可以正常通过了。
yarn test effect