手撸mini-vue之实现effect的stop功能

278 阅读2分钟

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;
}

源码地址戳这里