完善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);
}

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

相关推荐
恋猫de小郭41 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端