前言
官方文档对computed
的定义:
接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。
另外,众所周知,计算属性具有缓存功能,因此我们可以将实现computed()
方法的需求归纳为:
computed
接受的一个fn
,返回值要用.value
来访问computed
缓存功能,当它的响应式依赖更新时才会重新执行fn
,否则不会被调用。
单测
找来源码中,满足这两点需求的单测。新建computed.spec.ts
:
ts
import { computed } from "../computed";
import { reactive } from "../reactive";
describe("computed", () => {
it("should return updated value", () => {
const value = reactive({});
const cValue = computed(() => value.foo);
expect(cValue.value).toBe(undefined);
value.foo = 1;
expect(cValue.value).toBe(1);
});
it("should compute lazily", () => {
const value = reactive({});
const getter = jest.fn(() => value.foo);
const cValue = computed(getter);
// lazy
expect(getter).not.toHaveBeenCalled();
expect(cValue.value).toBe(undefined);
expect(getter).toHaveBeenCalledTimes(1);
// should not compute again
cValue.value;
expect(getter).toHaveBeenCalledTimes(1);
// should not compute until needed
value.foo = 1;
expect(getter).toHaveBeenCalledTimes(1);
// now it should compute
expect(cValue.value).toBe(1);
expect(getter).toHaveBeenCalledTimes(2);
// should not compute again
cValue.value;
expect(getter).toHaveBeenCalledTimes(2);
});
});
首先,先实现第一个测试用例should return updated value
应该返回更新后的值。
实现第 1 个测试用例
将第二个测试用例跳过,添加skip
方法,it.skip("should compute lazily",...)
第一个测试用例中,定义了一个空的响应式对象value
,计算属性返回其中的foo
属性,那第一次访问计算属性值的时(通过.value
形式)应该获得undefined
。当响应式对象更新了foo
属性的值为 1,计算属性也做相应的修改。
新建computed.ts
ts
class ComputedRefImpl {
private _getter: any;
constructor(getter) {
this._getter = getter
}
get value() {
return this._getter();
}
}
export function computed(getter) {
return new ComputedRefImpl(getter);
}
computed
返回值需要通过.value
,实现起来和ref
一样,都通过class
类来进行对象模拟,可以实现数据拦截。其返回值就是传入的getter
函数的执行结果。
如果此时执行单测yarn test computed
就会发现第 1 个断言expect(cValue.value).toBe(undefined)
成功通过,报错发生在第 2 个断言,因为我们还未处理响应式更新之后的逻辑。
可以分析一下,当响应式对象value
中foo
更新为 1,需要计算属性相应的返回 1。那相当于依赖的value
在更新时computed
也会触发更新。那此前实现的响应式依赖处理的函数就是effect
,这里就可以复用一下。
将effect.ts
中ReactiveEffect
类导出,修改上面的ComputedRefImpl
ts
import { ReactiveEffect } from "./effect";
class ComputedRefImpl {
private _effect: any;
constructor(getter) {
this._effect = new ReactiveEffect(getter);
}
get value() {
return this._effect.run();
}
}
当响应式数据value
更新时会触发它的trigger
方法,trigger
方法中会将dep
中所有effect
执行,此时执行了effect.run()
方法再将这个结果返回到computed
的get value
中,也就是顺利的实现了同步更新。
执行单测yarn test computed
,第一个测试用例通过。
实现第 2 个测试用例
放开skip
,直接执行单测,
发现问题,解决问题。
首先来分析一下这个测试用例,具体都测试了些什么。
ts
it("should compute lazily", () => {
const value = reactive({});
const getter = jest.fn(() => value.foo);
const cValue = computed(getter);
// lazy
expect(getter).not.toHaveBeenCalled();
expect(cValue.value).toBe(undefined);
expect(getter).toHaveBeenCalledTimes(1);
// should not compute again
cValue.value;
expect(getter).toHaveBeenCalledTimes(1);
// should not compute until needed
value.foo = 1;
expect(getter).toHaveBeenCalledTimes(1);
// now it should compute
expect(cValue.value).toBe(1);
expect(getter).toHaveBeenCalledTimes(2);
// should not compute again
cValue.value;
expect(getter).toHaveBeenCalledTimes(2);
});
定义了一个空的响应式对象value
,jest
模拟了一个computed
内的getter
函数。当没有访问computed
值时,getter
函数不会被调用;当访问computed
值时,会被调用 1 次;再次去访问computed
值时,getter
不会被调用,调用次数仍然是 1 ; 响应式数据value
更新,getter
仍然不会被触发;但再去访问计算属性时,getter会被调用获取最新值;再次访问时就不再调用了。
具体实现:
kotlin
import { ReactiveEffect } from "./effect";
class ComputedRefImpl {
private _effect: any;
private _dirty: boolean = true;
private _value: any;
constructor(getter) {
this._effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true;
}
});
}
get value() {
if (this._dirty) {
this._dirty = false;
this._value = this._effect.run();
}
return this._value;
}
}
使用一个布尔值变量_dirty
来控制何时可以去访问computed
值,也就get value
中的_effect.run()
的触发,调用过一次就关闭_dirty
表示下一次不给触发了,并将结果缓存起来,不调用执行effect
时直接返回这个缓存值。再借助ReactiveEffect
类中scheduler
变量,存在scheduler
时触发trigger
中执行的就是scheduler
的函数,就可以在这个函数里操作_dirty
再次开启,以便下次访问computed
时会继续触发get value
,从而更新计算属性的值和响应式数据保持一致。
执行单测yarn test computed
,测试通过。