开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 11 天,点击查看活动详情
背景
本文记录笔者实现 mini-vue3 项目中 effect 模块的 scheduler 功能。
根据 学习方法,先将功能拆分成多个功能点,列举出来。
功能点
effect函数接收2个参数,第1个参数是依赖函数fn,第2个参数是一个options对象,options里有一个名为scheduler的函数,首次执行effect时调用fn,但是不会调用scheduler。- 当响应式对象
UPDATE时不再执行fn了,而是执行scheduler。 effect函数执行后会返回一个runner函数,执行runner会再次执行fn。
编写代码
功能点1
第 1 个功能点是:effect 函数接收 2 个参数,第 1 个参数是依赖函数 fn,第 2 个参数是一个 options 对象,options 里有一个名为 scheduler 的函数,首次执行 effect 时调用 fn,但是不会调用 scheduler。
编写测试用例
通过 jest 的 it 方法定义一个新的测试用例 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定义):
- 首次执行
effect时调用fn,所以valueForFn是当前obj.foo的值,也就是1。 - 首次执行
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,结果如下:
可以看到,测试通过 ✅,功能点1已实现 🎉
功能点2
第 2 个功能点是:当响应式对象 UPDATE 时不再执行 fn,而是执行 scheduler。
编写测试用例
上一步骤已经创建了一个响应式对象 obj,通过 obj.foo++ 可以触发 UPDATE。
由于响应式对象 UPDATE 时 fn 不执行,所以测试用例需验证valueForFn 仍旧等于 1。
由于响应式对象 UPDATE 时 scheduler 执行,所以测试用例需验证 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 执行测试
测试通过✅,功能点2已实现 🎉
功能点3
功能点3是:执行 effect 后会返回一个 runner 函数,执行 runner 函数会执行 fn。
编写测试用例
首先通过一个 runner常量 接收 effect 的返回值,它必须是一个函数。
// effect.spec.ts
const runner = effect(
() => {
valueForFn = obj.foo;
},
{ scheduler }
)
obj.foo++;
// ...
然后,执行 runner,这时要验证 fn 是否执行,由于前面 obj.foo 是1,中间让 obj.foo++ 了,所以这里验证它等于 2 即可。
// 执行 runner
runner();
// 任务点3. 测试1: 执行 runner 会再次执行 fn
// 所以 valueForFn === obj.foo === 2 (obj.foo 已经累加过了,值是 2)
expect(valueForFn).toBe(2);
测试驱动开发
根据测试用例,主要开发 2 点:
effect要返回一个函数runner。- 执行
runner要能调用fn。
ReactiveEffect 的 run 方法就能满足 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已实现 🎉
总结
本文实现 effect函数 返回 runner函数 的功能,分为 3 个功能点,每个功能点都按照测试驱动开发的思路来完成,最终实现了整体功能。
本步骤完整代码可在笔者的远程库 ---- mini-vue3: 实现 effect 的 scheduler 功能 中查看。