【崔学社】@MINI-VUE@reactive-stop,runner,sechduler

397 阅读5分钟

我们先来实现一下 runner runner是 effect 的功能 调用effect 之后 会返回一个 runner 当我们再去调用这个 runner 函数的时候 他还是 会去继续执行这个这个effect 我们先看一下 runner的 test 而且 如果我们又返回值的话 我们调用函数 回去执行这个effect 而且 会返回 effect 返回的值

  it("show return runner when call effect", () => {
    let foo = 10;
    const runner = effect(() => {
      foo++;
      return "foo";
    });
    expect(foo).toBe(11);
    const r = runner();
    expect(foo).toBe(12);
    expect(r).toBe("foo");
  });

我们 调用 runner 的时候 就是把effect 中的fn 再次调用一边 我们就可以直接 返回 一个调用的run方法 但是我们当前 effect实例需要 传一个this 所以我们需要显示绑定this 保证我们返回的是 当前的 那个effect

export function effect(fn, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler);
  const runner = _effect.run.bind(_effect);
  return runner;
}

而且我们在调用 用户 的 fn之后 需要返回当前实例的 值 所以我们可以直接 在run方法中直接返回


class ReactiveEffect {
  private _fn:any;
  constructor(fn) {
    this._fn = fn
  }
  run() {
    // 我们执行当前的run 之后 我们 就需要 保存我们 依赖 利用我们的全局变量获取
    activeEffect = this  // 这个 this 值得 是我们当前实例对象 就是这个我们effect 传入的fn
    return this._fn() // 直接return 拿到 effect 实例的 fn 的返回值
  }
}

我们接下来 实现 sechduler api 同样我们先来看单元测试

  test("scheduler", () => {
    // 通过effect的第二个参数 给定一个 scheduler 的fn 当effect 第一次执行的时候还会执行fn
    // effect 第一次执行的之后 会默认执行 fn
    // 当响应式对象set 之后 不会执行fn 而是会执行scheduler
    // 如果我们当时在执行runner的时候 会再次执行effect的第一个参数
    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 第一次trigger
    obj.foo++; // 如果我们set之后 如果有scheduler 他回去执行scheduler 而不会去调用第一个 effect 参数
    expect(scheduler).toHaveBeenCalledTimes(1);
    // 不会再去执行run
    expect(dummy).toBe(1);
    run();
    expect(dummy).toBe(2);
  });

effect函数中传递的第二个参数sechduler 一开始不回去被调用 默认 effect 中的fn 会被调用一次 所以第一次 我们可以直接 拿到 dummy 等于一 然后 当响应式 的 值 发生改变的时候 我们才回去调用这个sechduler 然后我们再去执行 run方法 调用runner dummy 等于二 重点就在于 我们 set 触发依赖的时候 我们如果判断又sechduler 那么我们就会去执行 sechduler

export function triggerEffects(dep) {
  for (const effect of dep) {
    if (effect.scheduler) {
      effect.scheduler();
    } else {
      effect.run();
    }
  }
}

我们的目标就是 在我们 tigger的时候 我们就去执行这个 sechduler 那么我们如何 能让 effect 中拿到这个sechduler呢

export class ReactiveEffect {
  private _fn;
  deps = [];
  onStop?: () => void;
  active = true;
  constructor(fn, public scheduler?) {
    this._fn = fn;
    this.scheduler = scheduler;
  }
  run() {
    // 这个时候收集依赖 但是因为 在 ++操作 会调用 get操作 还是会 收集依赖 所以我i们需要一个变量来判断
    if (!this.active) {
      return this._fn();
    }

    shouldTrack = true;
    activeEffect = this;
    const result = this._fn();
    // 重置
    shouldTrack = false;
    return result;
  }
}

我们可以在effect 类中定义一个 public 的 scheduler 就可以直接获取了 到时候 就是 我们在传递fn的时候 多加一个参数 就是 option.scheduler

  const _effect = new ReactiveEffect(fn, options.scheduler);

接下来我们去写 stop , onStop 功能 首先我们先看stop 的单元测试

1. 我们的 reactive 内部存在一个容器 deps 我们的effect 是 传入fn 我们把 fn存在了deps

2. 在我们触发set操作的时候就会把 deps里面所有的 effect 中的fn 拿来全部执行

3. 那么我们如何 让 调用stop的时候 不再去 调用整个 fn 呢 我们就可以 删除 整个dep

  test("stop", () => {
    let dummy;
    const obj = reactive({ prop: 1 });
    const runner = effect(() => {
      dummy = obj.prop;
    });
    obj.prop = 2;
    // expect(dummy).toBe(2);
    // expect(runner()).toBe(undefined);
    stop(runner);
    obj.prop++; //  这块 有问题
    // obj.prop = 3;
    expect(dummy).toBe(2);
    // 但是我们如果调用runner之后 他还是会继续被调用
    // runner();
    // expect(dummy).toBe(3);
  });

我们实现stop 函数 里面传递的参数 就是 runner 函数的返回值 那么我们 如何 给 runner 添加 stop 方法呢 我们可以 给 runner传递一个 effect这样就能拿到 当前实例对象 然后去声明一个 stop 方法 执行

export function stop(runner) {
  return runner._effect.stop();
}
export function effect(fn, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler);

  // _effect.onStop = options.onStop;  方式不优雅  优雅写法在下
  // Object.assign(_effect, options); // 还是不具有语义化
  extend(_effect, options);
  _effect.run();
  const runner = _effect.run.bind(_effect);
  runner._effect = _effect;
  return runner;
}

然后我们实现stop方法 我们需要获取 当前 runner 对应的这个effect 才能 对应删除 我们如何 获取呢 我们实例dep 实在 trigger 的时候 添加的 那么 我们新建一个 数组 也同样保存一下

export class ReactiveEffect {
  private _fn;
  deps = [];
  onStop?: () => void;
  active = true;
  constructor(fn, public scheduler?) {
    this._fn = fn;
    this.scheduler = scheduler;
  }
  run() {
    // 这个时候收集依赖 但是因为 在 ++操作 会调用 get操作 还是会 收集依赖 所以我i们需要一个变量来判断
    if (!this.active) {
      return this._fn();
    }

    shouldTrack = true;
    activeEffect = this;
    const result = this._fn();
    // 重置
    shouldTrack = false;
    return result;
  }
  stop() {
    cleanupeffect(this)
    }
  }
}
function cleanupEffect(currentEffect) {
  currentEffect.deps.forEach((activeEffect) => {
    activeEffect.delete(currentEffect);
  });
  // 优化点  已经 清空完了 deps 的存在就没有意义了
  currentEffect.deps.length = 0;
}

我们现在就是传入了 一个 cleaneffect 方法 传入一个 参数 代表 当前effect实例fn 我们只需要每次他在调用stop的时候 我们就可以在trigger的时候删除 这个effect 但是我们可以想一下 如果我们 频繁的调用stop 那么他每次都会区调用这个cleanupeffect函数 每次都会去循环 但是 其实 当前 实例 已经在第一次的时候 已经被删除了 所以我们可以 用一个变量来判断当前我们是否需要继续 去 调用这个stop里面的 clleanupeffect函数 我们 直接定义一个 类的变量 active

stop() {
    if (this.active) {
      cleanupEffect(this);
      if (this.onStop) {
        this.onStop();
      }
      this.active = false;
    }
}