【Vue3源码】第二章 effect功能的完善
前言
上一章节我们实现了基础版本的effect函数,和reactive函数并且实现依赖收集和依赖触发功能,这一章我们继续完善effect函数的功能。了解vue真正的强大!
如果你还不了解单元测试和环境搭建,请查看我的《【Jest】Jest单元测试环境搭建》内容!
1、实现effect返回runner
实现runner到底有什么作用呢?
我会在后面的文章实现computed详细介绍为什么Vue3源码要这样设计,这里先卖个关子
我们现在只需要知道,runner是effect函数的返回值,并且这个runner会返回effect的第一个参数fn。
我们先看下vue3源码中定义的effect返回的类型:
export interface ReactiveEffectRunner<T = any> {
(): T
effect: ReactiveEffect
}
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions //options第二节会详细讲,现在不用管它
): ReactiveEffectRunner { ... }
看了源码中的ReactiveEffectRunner
类型,我们已经了解了effect返回的类型,并且知道了这个返回值runner需要实现怎么样的功能。
找到effect函数进行改进。
export const effect = (fn) => {
const _effect = new ReactiveEffect(fn);
_effect.run();
//通过bind绑定this指向问题,让this显示指向_effect,这样runner就会有返回值。
const runner = _effect.run.bind(_effect);
return runner
};
上面代码如果直接返回出
_effect.run()
就会返回'foo'
。因为this指向问题,我们需要重新给run方法绑定this。我会在单元测试中断点测试这两个内容。
这是ReactiveEffect类中的run方法:
run() {
activeEffect = this;
return this._fn();
}
这样effect函数就会显示绑定到_effect
实例本身,返回_effect
实例中的的run方法。
现在的代码只是实现了effect函数有返回值而已并不能得到runner返回值。我们还需要在ReactiveEffect
类中修改下run方法,让run方法可以返回_fn调用后的结果。
直接在run方法中返回函数调用的结果即可
class ReactiveEffect {
private _fn;
constructor(fn) {
this._fn = fn;
}
run() {
activeEffect = this;
//返回_fn的返回值
return this._fn();
}
}
runner的单元测试
编写完代码逻辑后我们就可以开始单元测试了。
在effect.spec.ts中添加以下测试代码
import { effect } from "../effect";
import { reactive } from "../reactive";
describe("effect", () => {
//测试runner
it("should return runner when call effect", () => {
//1.effect(fn) => function (runner) => fn => return
let foo = 10;
//使用常量接收effect的返回值
const runner = effect(() => {
foo++;
return "foo";
});
expect(foo).toBe(11);
// 使用r接收runner的返回值
const r = runner();
expect(foo).toBe(12);
//r成功接到了effect中第一个参数的返回值“foo”
expect(r).toBe("foo");
});
});
我们在命令行输入 yarn test 测试,通过单元测试!
这是上文提到的断点的截图:
return _effect.run()
和 return _effet.run.bind(_effect)
的区别:
我们成功实现了runner的功能,effect方法会返回一个ReactiveEffectRunner类型的返回值,这个返回值(runner)可以再次调用,返回effect传入的第一个参数 即 fn的返回值。
2、scheduler功能
从上文中我们了解到,effect函数其实还传入第二参数options,源码中的options参数是一个ReactiveEffectOptions
类型
我们看一下源码中的这个类型
export interface ReactiveEffectOptions extends DebuggerOptions {
lazy?: boolean
scheduler?: EffectScheduler
scope?: EffectScope
allowRecurse?: boolean
onStop?: () => void
}
export type EffectScheduler = (...args: any[]) => any
ReactiveEffectOptions
类型中,找到scheduler
,他的值是一个函数。
effect第二参数中的scheduler属性的作用:
当effect传入option中包含一个scheduler属性时,那么第一参数fn只会被执行一次,接下来每次的set操作时我们不再去tigger中触发run方法,而是执行我们的scheduler公共方法,我们只有在调用runner时才能继续触发run方法,来实现响应式。
这里我们就知道为什么第一步要封装effect的返回值runner了,为了控制run方法的执行,而不是每次set操作时一直的触发run方法,(当然get操作中我们没有限制去收集依赖),scheduler(调度程序)和 runner 帮助 Vue去实现了可以控制的执行run方法的功能!
下面就可以开始一一实现scheduler功能的代码了!
1. 修改effect函数
把effect的第二个参数传入到
ReactiveEffect
类中。
export const effect = (fn, options:any = {}) => {
//我们实例化时也要携带options
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
return _effect.run.bind(_effect);
};
2. 修改ReactiveEffect类
让该类的构造函数接收scheduler传参,并且定义成public公共属性
class ReactiveEffect {
private _fn;
//在构造函数中我们接收一个公共的scheduler参数,这样外部就能使用它了
constructor(fn, public scheduler?) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
activeEffect = this;
return this._fn();
}
}
上面两步的修改主要是为了实现scheduler函数在实例外部也可以访问。
- 修改trigger函数
当我们effect函数传入的第二参数options中包含scheduler时,并且reactive函数触发set操作时调用trigger函数中,不让
ReactiveEffect
的实例去调用run方法,,而是触发scheduler函数来实现控制run执行的目的!
//依赖触发
export function trigger(target, key) {
let depsMap = targetMap.get(target);
let dep = depsMap.get(key);
for (let effect of dep) {
// 当触发set时,如果有scheduler就执行scheduler,收集的依赖永远无法触发
if (effect.scheduler) {
effect.scheduler();
// 没有就触发ReactiveEffect实例的run方法依赖触发
} else {
effect.run();
}
}
}
runner和scheduler一起单元测试
我们对两个功能一起进行单元测试,当然以前写的reactive函数的功能也不会受到影响
import { effect } from "../effect";
import { reactive } from "../reactive";
describe("effect", () => {
it("happy path", () => {
const user = reactive({
age: 10,
name: "www",
newObj: {
objAge: 11,
},
});
let nextAge;
let age2;
effect(() => {
nextAge = user.age + 1;
});
//无法代理深层嵌套的函数
effect(() => {
age2 = user.newObj.objAge;
});
expect(nextAge).toBe(11);
user.age++;
expect(nextAge).toBe(12);
user.age = 99;
expect(nextAge).toBe(100);
expect(age2).toBe(11);
//对于深层嵌套的对象由于没有封装递归的逻辑所以监听不到
user.newObj.objAge++;
//理论上来说应该变成12,而结果却没有变化
expect(age2).toBe(11);
});
it("should return runner when call effect", () => {
//1.effect(fn) => function (runner) => fn => return
let foo = 10;
const runner = effect(() => {
foo++;
return "foo";
});
expect(foo).toBe(11);
const r = runner();
expect(foo).toBe(12);
expect(r).toBe("foo");
});
it("scheduler",() => {
// 1. 通过 effect 的第二个参数给定一个 scheduler (是一个函数类型的参数)
// 2. effect 第一次执行的时候 就会执行第一个参数中的 这个 函数
// 3. 当响应式对象 set update 不会执行第一个参数的 fn 而是 执行第二个 scheduler 的函数
// 4. 而当执行runner时才会再次执行 fn
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).toHaveBeenCalledTimes(1)
//should not run yet
expect(dummy).toBe(1)
//manually run
run()
//should have run
expect(dummy).toBe(2)
})
});
执行yarn test,三次单元测试全部通过!!
我们可以发现结果非常的神奇,有了runner和scheduler,我们实现了一个通过options配置项来控制trigger依赖触发执行run还是scheduler的功能!
本文篇幅过长,请查看下一章《effect的功能完善下》~