「mini-vue3」实现 effect 的 scheduler 功能

256 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 11 天,点击查看活动详情

背景

本文记录笔者实现 mini-vue3 项目中 effect 模块的 scheduler 功能。

根据 学习方法,先将功能拆分成多个功能点,列举出来。

功能点

  1. effect 函数接收 2 个参数,第 1 个参数是依赖函数 fn,第 2 个参数是一个 options 对象,options 里有一个名为 scheduler 的函数,首次执行 effect 时调用 fn,但是不会调用 scheduler
  2. 当响应式对象 UPDATE 时不再执行 fn 了,而是执行 scheduler
  3. effect 函数执行后会返回一个 runner 函数,执行 runner 会再次执行 fn

编写代码

功能点1

1 个功能点是:effect 函数接收 2 个参数,第 1 个参数是依赖函数 fn,第 2 个参数是一个 options 对象,options 里有一个名为 scheduler 的函数,首次执行 effect 时调用 fn,但是不会调用 scheduler

编写测试用例

通过 jestit 方法定义一个新的测试用例 scheduler,编写测试代码:

// effect.spec.ts

describe('effect', () => {
    
  it('scheduler', () => {
    // 定义一个响应式对象
    const obj = reactive({ foo: 1 })

    // 通过 jest.fn 创建一个模拟函数 scheduler。
    // 参考 https://jestjs.io/docs/jest-object#jestfnimplementation
    const scheduler = jest.fn(() => {})

    // 检测 fn 是否执行的变量
    let valueForFn = 0;
    effect(
      () => {
        valueForFn = obj.foo;
      },
      { scheduler }
    )

    expect(valueForFn).toBe(1);
    expect(scheduler).not.toHaveBeenCalled();
  })

})

上面测试用例中有 2 个测试项 (expect定义):

  1. 首次执行 effect 时调用 fn,所以 valueForFn 是当前 obj.foo 的值,也就是 1
  2. 首次执行 effect 时,不调用 scheduler,参考 toHaveBeenCalled

测试驱动开发

接下来根据测试用例编写功能

effect 接收 scheduler

effect.ts 中,先让 effect 函数新增一个参数 options,用来接收 scheduler 函数。

然后,将 options.scheduler 传递给 ReactiveEffect 构造函数。

// ...

interface IOption {
  scheduler: Function;
}

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

// ...

同时要修改 ReactiveEffect 类,使它能接收 scheduler 函数,并作为 public 属性暴露给实例使用。

class ReactiveEffect {

  private _fn: Function;

  // 接收 scheduler 函数
  constructor(fn: Function, s: Function) {
    this._fn = fn;
  }

  // ...
}

通过 jest 执行测试

执行 yarn test effect,结果如下:

j1.png

可以看到,测试通过 ✅,功能点1已实现 🎉

功能点2

2 个功能点是:当响应式对象 UPDATE 时不再执行 fn,而是执行 scheduler

编写测试用例

上一步骤已经创建了一个响应式对象 obj,通过 obj.foo++ 可以触发 UPDATE

由于响应式对象 UPDATEfn 不执行,所以测试用例需验证valueForFn 仍旧等于 1

由于响应式对象 UPDATEscheduler 执行,所以测试用例需验证 scheduler 执行了,因为此时 scheduler 是一个 mock function,笔者通过 toHaveBeenCalledTimes 来验证 scheduler 执行次数是否为 1 即可。

结合功能点1,2,测试代码如下:

// effect.spec.ts

describe('effect', () => {
    
  it('scheduler', () => {
    // 定义一个响应式对象
    const obj = reactive({ foo: 1 })
    // 通过 jest.fn 创建一个模拟函数 scheduler。
    // 参考 https://jestjs.io/docs/jest-object#jestfnimplementation
    const scheduler = jest.fn(() => {})

    // 检测 fn 是否执行的变量
    let valueForFn = 0;
    effect(
      () => {
        valueForFn = obj.foo;
      },
      { scheduler }
    )

    expect(valueForFn).toBe(1);
    expect(scheduler).not.toHaveBeenCalled();
    
    // 任务点2. 当响应式对象 `UPDATE` 时不再执行 `fn`,而是执行 `scheduler`。
    // 响应式对象 UPDATE
    obj.foo++;
    // 任务点2. 测试1: 响应式对象 UPDATE 时,不执行 fn,所以 valueForFn 依旧是 1。
    expect(valueForFn).toBe(1);
    // 任务点2. 测试2: 响应式对象 UPDATE 时,执行 scheduler,使用 toHaveBeenCalledTimes(1) 确定 scheduler 被执行了 1 次。
    // 参考 https://jestjs.io/docs/expect#tohavebeencalledtimesnumber
    expect(scheduler).toHaveBeenCalledTimes(1);
  })

})

测试驱动开发

响应式对象 UPDATE 时,触发的是 reactivity 模块定义的 SET 方法,其中调用的是 trigger 函数,所以我们从 trigger 入手改造。

但是,trigger 使用的是 effect 结点,要调用 scheduler 需要从 effect 上暴露出方法,所以笔者先改写 ReactiveEffect 类:

class ReactiveEffect {
    
  // 暴露 scheduler 方法
  public scheduler: Function;

  private _fn: Function;

  constructor(fn: Function, s: Function) {
    this._fn = fn;
    this.scheduler = s;
  }

  // ...
}

由于响应式对象 UPDATE 时不执行 fn,而是执行 scheduler,所以现在改造 trigger 函数:

export function trigger(target: Object, key: string) {
  let keyMap = targetMap.get(target);
  if (!keyMap) return;

  let effectSet = keyMap.get(key);
  if (!effectSet) return;
    
  for (const effect of effectSet) {
    if (effect.scheduler) {
      // 本来只是执行 run,现在添加 scheduler。
      effect.scheduler();
    } else {
      effect.run();
    }
  }
}

通过 jest 执行测试

j2.png

测试通过✅,功能点2已实现 🎉

功能点3

功能点3是:执行 effect 后会返回一个 runner 函数,执行 runner 函数会执行 fn

编写测试用例

首先通过一个 runner常量 接收 effect 的返回值,它必须是一个函数。

// effect.spec.ts
const runner = effect(
  () => {
    valueForFn = obj.foo;
  },
  { scheduler }
)

obj.foo++;
// ...

然后,执行 runner,这时要验证 fn 是否执行,由于前面 obj.foo1,中间让 obj.foo++ 了,所以这里验证它等于 2 即可。

// 执行 runner
runner();
// 任务点3. 测试1: 执行 runner 会再次执行 fn
// 所以 valueForFn === obj.foo === 2 (obj.foo 已经累加过了,值是 2)
expect(valueForFn).toBe(2);

测试驱动开发

根据测试用例,主要开发 2 点:

  1. effect 要返回一个函数 runner
  2. 执行 runner 要能调用 fn

ReactiveEffectrun 方法就能满足 runner 函数的需求,所以只需在 effect 函数的最后返回 run 方法即可,注意 run 中使用了 this,所以返回前要 bind_effect 这个上下文

export function effect(fn: Function, options: IOption) {
  const _effect = new ReactiveEffect(fn, options.scheduler);
  // 首次执行 effect 就执行 fn
  _effect.run();
  // 返回 runner 函数
  return _effect.run.bind(_effect);
}

通过 jest 执行测试

3.png

测试通过✅,功能点3已实现 🎉

总结

本文实现 effect函数 返回 runner函数 的功能,分为 3 个功能点,每个功能点都按照测试驱动开发的思路来完成,最终实现了整体功能

本步骤完整代码可在笔者的远程库 ---- mini-vue3: 实现 effect 的 scheduler 功能 中查看。