完善effect功能

基于上一篇文章中实现的effect方法,根据 Vue3 源码中单测,完善该方法的三点功能,分别是:

  1. runner : effect可以返回自执行的入参runner函数
  2. scheduler : effect支持添加第二个参数选项中的scheduler功能
  3. stop : effect添加stop功能

runner

单测

effect.spec.ts文件中添加关于runner的测试用例。

ts 复制代码
it("should be return runner when call effect", () => {
  let foo = 1;
  const runner = effect(() => {
    foo++;
    return "foo";
  });

  expect(foo).toBe(2);

  const r = runner();
  expect(foo).toBe(3);
  expect(r).toBe("foo");
});

上面测试用例的意思是,effect内部的函数会自执行一次,foo的值变成2。effect是一个可执行函数runner,执行runnereffect内部函数也会执行,因此foo的值会再次自增变成3,并且runner的返回值就是effect内部函数的返回值。

实现

effect函数需要可以返回它的入参执行函数,且内部执行函数可以返回。

ts 复制代码
class ReactiveEffect {
  private _fn: any;
  constructor(fn) {
    this._fn = fn;
  }
  run() {
    reactiveEffect = this;
    return this._fn();
  }
}

export function effect(fn) {
  let _effect = new ReactiveEffect(fn);
  _effect.run();

  const runner = _effect.run.bind(_effect)
  return runner;
}

需要注意的是,这里存在this指向的问题,在返回_effect.run函数时需要绑定当前实例。

验证

执行yarn test effect

scheduler

单测

ts 复制代码
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).toHaveBeenCalled();
  // should not run yet
  expect(dummy).toBe(1);
  // manually run
  run();
  // should have run
  expect(dummy).toBe(2);
});

上面测试用例代码的意思是:effect方法接收第二个参数,是一个选项列表对象,其中有一个是scheduler,是一个函数。这里用jest.fn模拟了一个函数将变量run赋值成runner函数。在第一次执行的时候,scheduler函数不调用执行,effect的第一个参数函数自执行,所以dummy赋值为1;当响应式对象变化时,也就是obj.foo++时,scheduler会被执行,但是dummy的值还是1,说明第一个参数函数并没有执行;run执行,也就是effect返回函数runner执行时,第一个参数函数执行,因为obj.foo++,所以dummy变成2。

可以总结出scheduler包含的需求点:

  1. 通过effect的第二个参数给定一个schedulerfn
  2. effect第一次执行的时候,执行第一个参数function
  3. 当响应式对象触发set操作时,不会执行function,而执行scheduler
  4. 当执行runner时,会再次执行function

实现

首先是effect函数可以接收第二个对象参数。

ts 复制代码
export function effect(fn, options: any = {}) {
  let _effect = new ReactiveEffect(fn, options.scheduler);
  _effect.run();

  const runner = _effect.run.bind(_effect)
  return runner;
}

Class类中也要相应的接收scheduler

ts 复制代码
class ReactiveEffect {
  private _fn: any;
  public scheduler: Function | undefined;
  constructor(fn, scheduler) {
    this._fn = fn;
    this.scheduler = scheduler;
  }
  run() {
    reactiveEffect = this;
    return this._fn();
  }
}

当响应式对象触发set操作时,也就是触发依赖时,在trigger方法中,执行scheduler,只需要判断是否存在scheduler,存在即执行。

ts 复制代码
export function trigger(target, key) {
  let depMap = targetMap.get(target);
  let dep = depMap.get(key);
  for (const effect of dep) {
    if (effect.scheduler) {
      effect.scheduler();
    } else {
      effect.run();
    }
  }
}

验证

stop

单测

ts 复制代码
import { effect, stop } from "../reactivity/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;
  expect(dummy).toBe(2);

  // stopped effect should still be manually callable
  runner();
  expect(dummy).toBe(3);
});

it("onStop", () => {
  const onStop = jest.fn();
  const runner = effect(() => {}, { onStop });
  stop(runner);
  expect(onStop).toHaveBeenCalled();
});

stop功能有两个测试用例,对应不同的功能,我们逐个分析。

"stop"中,effect内函数自执行一次,所以第一次断言dummy为上面赋值的2;执行stop方法,stop方法是来自effect对外暴露的方法,它接收runner函数作为参数,即便再更新响应式对象,effect内函数也不执行,dummy仍然是2;再次执行runner,恢复执行effect内函数,dummy变成了3。

总结来说,stop可以阻止effect内函数执行。

"onStop"中,effect函数接收第二个参数对象中有个属性是onStop,且接收一个函数,当执行stop时,onStop函数会被执行。

实现

触发依赖时,trigger方法中循环执行了dep中所有的effect内方法,那需要阻止执行,就可以从dep中删除该项。

首先stop方法接收runner函数作为参数。

ts 复制代码
export function stop(runner) {
  runner.effect.stop();
}

runner函数上挂载一个effect实例,就可以获取到 Class 类中定义的stop方法。

ts 复制代码
class ReactiveEffect {
  private _fn: any;
  public scheduler: Function | undefined;
  constructor(fn, scheduler) {
    this._fn = fn;
    this.scheduler = scheduler;
  }
  run() {
    reactiveEffect = this;
    return this._fn();
  }
  stop() {}
}

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

  _effect.run();

  const runner: any = _effect.run.bind(_effect);
  runner.effect = _effect; // 挂载effect实例

  return runner;
}

那如何从dep中删除需要阻止执行的一项呢?

track方法中dep.add(reactiveEffect)建立了dep这个Set结构和effect实例的关系,但是在 Class 类中并没有实例和dep的映射关系,因此可以Class类中定义一个deps数组用来存放该实例的所有dep,在需要调用stop方法时将删除dep中的该effect实例方法。

ts 复制代码
class ReactiveEffect {
  private _fn: any;
  public scheduler: Function | undefined;
  deps = []; 
  constructor(fn, scheduler) {
    this._fn = fn;
    this.scheduler = scheduler;
  }
  run() {
    reactiveEffect = this;
    return this._fn();
  }

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

export function track(target, key) {
  ...
  dep.add(reactiveEffect);
	reactiveEffect.deps.push(dep); // 存放deps
}

验证

优化

虽然单测通过了,但是代码是有优化空间的,我们来重构一下。

stop方法中逻辑可以抽离成一个单独函数。

ts 复制代码
class ReactiveEffect {
	...
  stop() {
    cleanupEffect(this);
  }
}

function cleanupEffect(effect) {
  effect.deps.forEach((dep: any) => {
    dep.delete(effect);
  });
}

性能上的优化,当用户一直调用stop方法,会导致这儿一直无故循环遍历,因此可以设置一个标志位来判断是否已经调用过执行了删除操作。

ts 复制代码
class ReactiveEffect {
  private _fn: any;
  public scheduler: Function | undefined;
  deps = [];
  active = true;
  constructor(fn, scheduler) {
    this._fn = fn;
    this.scheduler = scheduler;
  }
  run() {
    reactiveEffect = this;
    return this._fn();
  }
  stop() {
    if (this.active) {
      cleanupEffect(this);
      this.active = false;
    }
  }
}

重构后需要再次执行单测,确保没有破坏功能。

实现

来实现stop的第二个功能onStop

首先将onStop方法挂载effect实例上。

ts 复制代码
export function effect(fn, options: any = {}) {
  let _effect = new ReactiveEffect(fn, options.scheduler);
  _effect.onStop = options.onStop

  _effect.run();

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

  return runner;
}

当执行stop时,onStop函数会被执行。

ts 复制代码
class ReactiveEffect {
  private _fn: any;
  public scheduler: Function | undefined;
  deps = [];
  active = true;
  onStop?: () => void;
  constructor(fn, scheduler) {
    this._fn = fn;
    this.scheduler = scheduler;
  }
  run() {
    reactiveEffect = this;
    return this._fn();
  }

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

验证

优化

effect方法的第二个参数options可能存在很多选项,那每次都通过_effect.onStop = options.onStop挂载到实例上是不优雅的,因此可以抽离这块的逻辑,作为一个公共的方法。

在 src 下新建文件夹 shared,新建index.ts

ts 复制代码
export const extend = Object.assign;

那在effect中就可以使用extend方法更语义化表达。

ts 复制代码
export function effect(fn, options: any = {}) {
  let _effect = new ReactiveEffect(fn, options.scheduler);
  extend(_effect, options);

  _effect.run();

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

  return runner;
}

重构完再次执行yarn test effect验证是否破坏功能。

验证

最后需要执行全部的单测,验证新增功能对原有代码是否有破坏,执行yarn test

在执行reactive单测时,出现了如上的报错,提示reactiveEffect可能是undefined不存在deps

reactive.spec.ts中只是单纯的测试了reactive的核心功能,此时还没有涉及到effect方法,reactiveEffect的赋值是在effect自执行时触发的,因此是初始undefined状态。

ts 复制代码
export function track(target, key) {
  ...
  if (!reactiveEffect) return; // 边界处理
  dep.add(reactiveEffect);
  reactiveEffect.deps.push(dep);
}

最后再次验证,测试通过,功能完善成功。

相关推荐
软件技术NINI几秒前
html知识点框架
前端·html
深情废杨杨4 分钟前
前端vue-插值表达式和v-html的区别
前端·javascript·vue.js
GHUIJS4 分钟前
【vue3】vue3.3新特性真香
前端·javascript·vue.js
众生回避10 分钟前
鸿蒙ms参考
前端·javascript·vue.js
洛千陨11 分钟前
Vue + element-ui实现动态表单项以及动态校验规则
前端·vue.js
GHUIJS1 小时前
【vue3】vue3.5
前端·javascript·vue.js
&白帝&1 小时前
uniapp中使用picker-view选择时间
前端·uni-app
魔术师卡颂1 小时前
如何让“学源码”变得轻松、有意义
前端·面试·源码
谢尔登2 小时前
Babel
前端·react.js·node.js
ling1s2 小时前
C#基础(13)结构体
前端·c#