likes
comments
collection
share

06_实现effect的stop和onStop功能

作者站长头像
站长
· 阅读数 22

06_实现effect的stop和onStop功能

一、实现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;
  expect(dummy).toBe(2);
  runner();
  expect(dummy).toBe(3);
});

通过以上单测,可以很明显地看出来,可以通过stop函数传入runner去停止数据的响应式,而当重新手动执行runner的时候,数据又会恢复响应式。

(二)代码实现

从单测继续分析代码实现,通过stop函数传入runner,那就得继续回到effect.ts,首先导出一个stop函数。

export function stop(runner) {

}

再开始完善stop函数。

继续分析:

通过runner停止当前effect的响应式 → 也就是从收集到当前effectdep中将其删除,实际上是对effect 的操作,所以继续在ReactiveEffect上维护一个stop方法。

class ReactiveEffect {
  private _fn: any;

  // 在构造函数的参数上使用public等同于创建了同名的成员变量
  constructor(fn, public scheduler?) {
    this._fn = fn;
  }

  run() {
    activeEffect = this;
    return this._fn();
  }

  stop() {

  }
}

大致思路明白了,接下来解决第一个问题:如何通过runner找到ReactiveEffect的实例,然后去调用stop

答:在function effect() {}中将_effect挂载到runner上。

所以需要改写一下之前的代码:

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

  _effect.run();

  const runner: any = _effect.run.bind(_effect);
  runner.effect = _effect;

  return runner;
}

那么我们导出的stop函数的逻辑就清晰了。

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

再来完善ReactiveEffect类的stop函数,也就是解决第二个问题:如何从收集到当前effectdep中将其删除?

答:此时,我们并不知道当前effect存在于哪些dep中,所以考虑从track时入手,在dep收集activeEffect后,让activeEffect 反向收集dep,这样,就知道了当前effect所在的dep,接下来删掉就行了。

dep.add(activeEffect);
activeEffect.deps.push(dep);
class ReactiveEffect {
  private _fn: any;
  deps = [];

  // 在构造函数的参数上使用public等同于创建了同名的成员变量
  constructor(fn, public scheduler?) {
    this._fn = fn;
  }

  run() {
    activeEffect = this;
    return this._fn();
  }

  stop() {
    this.deps.forEach((dep: any) => {
      dep.delete(this);
    });
  }
}

功能完成后,继续看一下单测结果。

06_实现effect的stop和onStop功能

(三)代码优化

在完成功能以后,重新考虑对之前代码的实现。

  1. 代码可读性的问题

    抽离将当前依赖从收集到的dep中删除的逻辑,命名为cleanupEffect,然后在类ReactiveEffectstop 中,直接调用cleanupEffect(this)即可。

    function cleanupEffect(effect: any) {
      effect.deps.forEach((dep: any) => {
        dep.delete(effect);
      });
    }
    
  2. 性能问题

    当多次调用stop时,实际上第一次已经删除了,后续调用都没有实际意义,只会引起无意义的性能浪费。 所以考虑给其一个active状态,当被cleanupEffect后,置为false,不再进行再次删除。

    class ReactiveEffect {
      private _fn: any;
      deps = [];
      active = true;
    
      // 在构造函数的参数上使用public等同于创建了同名的成员变量
      constructor(fn, public scheduler?) {
        this._fn = fn;
      }
    
      run() {
        activeEffect = this;
        return this._fn();
      }
    
      stop() {
        // 要从收集到当前依赖的dep中删除当前依赖activeEffect
        // 但是我们根本不知道activeEffect存在于哪些dep中,所以就要用activeEffect反向收集dep
        if (this.active) {
          cleanupEffect(this);
          this.active = false;
        }
      }
    }
    

二、实现onStop

(一)单元测试

it('onStop', () => {
  const obj = reactive({ prop: 1 });
  const onStop = jest.fn();
  let dummy;
  const runner = effect(
    () => {
      dummy = obj.foo;
    },
    {
      onStop,
    },
  );

  stop(runner);
  expect(onStop).toBeCalledTimes(1);
});

其实通过单测,可以看出功能跟stop有些类似,逻辑也很简单,就是通过effect的第二个参数,给定一个onStop 函数,当有这个函数时,我们再去调用stop(runner) 时,onStop就会被调用一次。

那么实现思路也就很清晰了,我们首先得在ReactiveEffect类中去接收这个函数,然后调用stop的时候,手动调用一下onStop即可。

(二)代码实现:

class ReactiveEffect {
  private _fn: any;
  deps = [];
  active = true;
  // + 定义函数可选
  onStop?: () => void;

  // 在构造函数的参数上使用public等同于创建了同名的成员变量
  constructor(fn, public scheduler?) {
    this._fn = fn;
  }

  run() {
    activeEffect = this;
    return this._fn();
  }

  stop() {
    // 要从收集到当前依赖的dep中删除当前依赖activeEffect
    // 但是我们根本不知道activeEffect存在于哪些dep中,所以就要用activeEffect反向收集dep
    if (this.active) {
      cleanupEffect(this);
      // + 如果onStop有,就调用一次
      if (this.onStop) {
        this.onStop();
      }
      this.active = false;
    }
  }
}

export function effect(fn, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler);
  // + 接收onStop
  _effect.onStop = options.onStop;

  _effect.run();

  const runner: any = _effect.run.bind(_effect);
  runner.effect = _effect;

  return runner;
}

单测通过:

06_实现effect的stop和onStop功能

(三)代码优化

考虑到后续options可能还会传入很多其他选项,所以进行一下重构

Object.assign(_effect, options);

感觉语义化稍弱,所以,就抽离出一个extend方法,又考虑到这个方法可以抽离成一个工具函数,所以在src下建立shared目录,然后建立index.ts,专门放置各个模块通用的工具函数。

// src/shared/index.ts

export const extend = Object.assign;

extend(_effect, options);

当然,重构完以后,别忘了重新跑一下effect单测。

06_实现effect的stop和onStop功能

(四)解决问题的思路

可以看到effect的单测是通过的,那完成这一组功能后,继续完成的跑一下所有单测,看看是否对其他功能造成影响。

yarn test

果然,不出意外的话,出现意外了。

06_实现effect的stop和onStop功能

可以看到是reactivehappy path单测出了问题,而且activeEffect是个undefined,那我们回去重新看一下。

06_实现effect的stop和onStop功能

不难看出observed.foo也是触发了get操作,也就是触发了track去收集依赖,而此时并没有effect 包裹着的依赖存在,所以run不会执行,也就没有activeEffect,所以此时我们并不应该去收集依赖,所以增加一个判断。

if (!activeEffect) return;

dep.add(activeEffect);
activeEffect.deps.push(dep);

为了验证结果,再次跑一下全部的单测。

06_实现effect的stop和onStop功能

ps

这是一个 早起俱乐部

⭐️ 适合人群:所有想有所改变的人,可以先从早起半小时开始!抽出30分钟,从初心开始!! ⭐️ 没有任何其它意味,只是本人想寻找一起早起、志同道合的小伙伴。