『手写Vue3』Reactive 和 Effect

143 阅读4分钟

使用TDD(测试驱动开发)的方式手写Vue,一起学习Vue3源码,感谢@阿崔cxr。

项目地址:github.com/DanmoSAMA/m…

基本的 Reactive & Effect

先提供单元测试:

describe('reactive', () => {
  it('happy path', () => {
    const user = { age: 10 };
    const reactiveUser = reactive(user);
    // 原对象和套了reactived的对象不是同一个
    expect(reactiveUser).not.toBe(user);
    // 被reactive包裹的对象,能访问到原对象上的属性
    expect(reactiveUser.age).toBe(10);
  });
});

这里的原理比较简单,大家也都清楚,用了Proxy。然后需要重写getter和setter,其中在getter中做依赖收集,在setter中做依赖触发。

export function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      // 依赖收集
      track(target, key);
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      // 依赖触发
      const res = Reflect.set(target, key, value);
      trigger(target, key);
      return res;
    }
  });
}

在实现依赖收集和触发之前,先写好单元测试:

describe('effect', () => {
  it('happy path', () => {
    const user = reactive({ age: 10 });
    let nextAge;

    effect(() => {
      nextAge = user.age + 1;
      console.log(nextAge);
    });
    // effect中的fn先执行一次
    expect(nextAge).toBe(11);
    
    user.age++;
    // age自增的同时,effect又调用一次
    expect(nextAge).toBe(12);
  });
})

需要先实现effect函数,目前它暂时只接受fn一个参数,表示回调函数。根据测试的逻辑,effect中需要调用一次fn。

对于reactive响应式对象,如果在effect的回调函数中使用到它的属性,那么getter中需要做依赖收集。我们维护全局变量targetMap,键为target,值为depsMap。而depsMap的键为key,值为dep,dep是集合类型。

假设A是响应式对象,从targetMap可以取出A的depsMap。depsMap的key是A的各个属性,value是保存了依赖该属性的effect集合。

全局变量activeEffect表示当前活跃的effect,在执行run()时就已经把activeEffect设置为当前的effect,之后运行回调函数时,可能访问了响应式对象上的属性,于是进入getter中的依赖收集逻辑,执行到track()函数,再使用activeEffect。

let activeEffect;
export function effect(fn) {
  // 创建effect 
  const _effect = new ReactiveEffect(fn);
  // 执行回调
  _effect.run();
}

class ReactiveEffect {
  private _fn: Function;

  constructor(fn) {
    this._fn = fn;
  }
  run() {
    // 重要!
    activeEffect = this;
    // 执行回调
    this._fn();
  }
}

export function track(target, key) {
  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);
  }
  // 直接使用activeEffect
  dep.add(activeEffect);
}

export function trigger(target, key) {
  const depsMap = targetMap.get(target);
  const dep = depsMap.get(key);
  dep.forEach((effect) => {
    // 运行依赖该属性的所有回调函数
    effect.run();
  });
}

Effect 返回 Runner

单元测试:

  it('should return runner when call effect', () => {
    let foo = 10;
    // runner可以拿到回调函数
    const runner = effect(() => {
      foo++;
      return foo + 10;
    });
    expect(foo).toBe(11);
    const returnValue = runner();
    expect(foo).toBe(12);
    // 执行runner也能获取到回调函数的返回值
    expect(returnValue).toBe(22);
  });

可见,effect需要返回回调函数并赋值给runner,且runner要返回和回调函数一样的值。稍加修改如下:

class ReactiveEffect {
  // ...

  run() {
    activeEffect = this;
    // 返回回调函数
    return this._fn();
  }
}
export function effect(fn, options: effectOptions = {}) {
  const _effect = new ReactiveEffect(fn, scheduler, onStop);
  _effect.run();
  // 因为上面用到this,所以要用bind绑定this
  const runner: any = _effect.run.bind(_effect);
  // 返回回调函数
  return runner;
}

实现 Effect 的 Scheduler

单元测试:

  it('scheduler', () => {
    let dummy;
    let run: any;
    const scheduler = jest.fn(() => {
      run = runner;
    });
    const obj = reactive({ foo: 1 });
    const runner = effect(
      () => {
        dummy = obj.foo;
      },
      { scheduler }
    );
    expect(scheduler).not.toHaveBeenCalled();
    expect(dummy).toBe(1);
    // should be called on first trigger
    obj.foo++;
    expect(scheduler).toHaveBeenCalledTimes(1);
    // should not run yet
    expect(dummy).toBe(1);
    // manually run
    run();
    // should have run
    expect(dummy).toBe(2);
  });

即,effect可以接受一个options参数,options里面有很多属性,其中包括了Scheduler,它也是一个回调函数。当提供Scheduler时,effect的回调函数(第一个参数)立刻执行一次,但是Scheduler不执行。响应式对象触发依赖时,执行的是Scheduler。

使用Scheduler的目的是回调不立即执行,后续用来实现异步更新。

代码修改如下:

class ReactiveEffect {
  public scheduler: Function | undefined;

  constructor(fn, scheduler?) {
    this.scheduler = scheduler;
  }
  // ...
}

export function effect(fn, options: effectOptions = {}) {
  const { scheduler } = options;
  const _effect = new ReactiveEffect(fn, scheduler, onStop);
  // 第一次还是执行fn回调
  _effect.run();
  // ...
}

export function trigger(target, key) {
  const depsMap = targetMap.get(target);
  const dep = depsMap.get(key);
  dep.forEach((effect) => {
    if (!effect.scheduler) {
      effect.run();
    } else {
      // 依赖触发的时候执行Scheduler 
      effect.scheduler();
    }
  });
}

实现 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;
    // 目前不能支持obj.prop++
    // obj.prop++;
    expect(dummy).toBe(2);

    // stopped effect should still be manually callable
    runner();
    expect(dummy).toBe(3);
  });
  
  it('onStop', () => {
    const obj = reactive({
      foo: 1
    });
    const onStop = jest.fn(); // 在stop时会执行
    let dummy;
    const runner = effect(
      () => {
        dummy = obj.foo;
      },
      {
        onStop
      }
    );
    // 响应式对象改变后,停止运行回调(fn和scheduler)
    // 即:effect从该属性的dep集合中移除
    // 这样就无法通过effect.run()或effect.scheduler()执行回调了
    stop(runner);
    // stop时触发onStop回调
    expect(onStop).toBeCalledTimes(1);
  });

主要是在effect中保存deps,在runner中保存effect。调用stop时,首先从runner中获取到effect,然后调用effect类的stop()方法,然后进入cleanEffect,逻辑是遍历deps,把effect从每个集合中删除。

class ReactiveEffect {
  // 在effect中保存deps
  // 该effect的回调函数中都访问了响应式对象的哪些属性,把这些属性对应的dep集合加入deps
  deps = [];
  active = true;
  onStop: Function | undefined;

  constructor(fn, scheduler?, onStop?) {
    this.onStop = onStop;
  }
  stop() {
    // 如果多次调用stop可能引起性能问题,使用active保证只成功调用一次
    if (this.active) {
      cleanEffect(this);
      if (this.onStop) {
        // 执行onStop回调
        this.onStop();
      }
    }
    this.active = false;
  }
}

function cleanEffect(effect) {
  // 遍历deps,将effect从中移除
  effect.deps.forEach((dep: Set<ReactiveEffect>) => {
    dep.delete(effect);
  });
}

export function stop(runner) {
  runner.effect.stop();
}

export function effect(fn, options: effectOptions = {}) {
  const { scheduler, onStop } = options;
  const _effect = new ReactiveEffect(fn, scheduler, onStop);

  // 把effect作为runner的属性保存,从而能在stop函数中获取effect
  runner.effect = _effect;
}

export function track(target, key) {
  // ...
  
  dep.add(activeEffect);

  // 如果没有调用过effect函数,activeEffect为undefined,访问其deps属性会报错
  if (!activeEffect) return;

  activeEffect.deps.push(dep);
}