computed和ref很像,都是变量.value,不一样的地方可能就是computed有缓存
首先我们先实现一个computed的happy path测试用例:
describe('computed',()=>{
it("happy path",()=>{
// 和ref很像变量.value
// 缓存
const user = reactive({
age:1
})
const age = computed(()=>{
return user.age
})
expect(age.value).toBe(1)
})
})
实现这个用例就非常简单:
1 computed.ts中导出computed函数,然后computed函数接受一个getter函数
2 由于上面我们提到computed和ref很像,实现上其实也是通过computedImpl类来实现访问value属性的时候返回传入getter函数的返回值
再来实现第二个测试用例,这个测试用例有点长,我们分拆开来:
it("should compute lazily",()=>{
const value = reactive({
foo:1
})
const getter = jest.fn(()=>{
return value.foo
})
const cValue = computed(getter)
// lazy
expect(getter).not.toHaveBeenCalled()
expect(cValue.value).toBe(1)
expect(getter).toHaveBeenCalledTimes(1)
// should not computed again 这里需要把effect类进入进来 收集依赖
cValue.value
expect(getter).toHaveBeenCalledTimes(1)
// should not compute until needed trigger -> effect -> get 重新执行了 通过scheduler去实现不一值调用get,并且把_dirty变成true
value.foo = 2
expect(getter).toHaveBeenCalledTimes(1)
// now it should computed
expect(cValue.value).toBe(2)
expect(getter).toHaveBeenCalledTimes(2)
// should not computed again
cValue.value
expect(getter).toHaveBeenCalledTimes(2)
})
在should not computed again之前的测试代码,在第一版的computed中是都可以通过的。到了下面后由于cValue.value中触发了getter,下面toHaveBeenCalledTimes就为2了。computed中是具有缓存功能的,但是由于我们代码中还没有为computed做缓存,所以他的结果就会为2,就不通过了。
接下来就为我们computed做一个缓存的功能,
给computed引入一个初始值为true的_dirty变量值,然后在get value中根据_dirty是否为true,判断是否为第一次访问get value,这样就可以通过单元测了。(这样就变成了只能调用一次 get value,再次调用的时候我们就把上次的值返回出去)
接下来我们分析一下should not compute until needed的测试代码,我们运行改测试代码时候就直接跑出来报错了,如图
其实这个报错是因为value.foo = 2的时候去走了trigger,触发依赖了,但是我们前面get中又没有对依赖进行一个收集,自然就undefined了。还有一个重要的点就是,当我们value.foo = 2后,我们下一次访问value的值需要进行一个更新的,但是我们此时的_dirty一直都是false的状态了,这个也是需要更改的。 (分析完后,其实就是我们需要对computed中get里做依赖收集,然后还需要在他trigger的时候去更新_dirty的变量)
解决没有收集依赖报错问题:
1 在effect.ts中将ReactiveEffect类导出,在computedImpl中声明一个this._effect去接收ReactiveEffect中new出来的对象
2 在ReactiveEffect中入getter
3 get value中的this._getter就是this._effect.run执行的返回值
当我们处理完上面的步骤以后,会发现又有一个新的报错
为什么getter会被执行了两次呢,主要是因为在我们响应式对象的值发生变化之后会去执行this._effect.run然后就会重新执行到getter,那要怎么样才能让他不执行两边getter,而且我们还需要在set完之后给this._drity变量变为true,这时候就想到了我们前面实现的scheduler,回顾一下scheduler是干什么的
scheduler: effect第一次执行走fn,当响应式对象发生set或update时候就,就走effect中传入的scheduler
其实真正写的代码并不多,只是computed的实现非常的巧妙,下面我先把涉及到的的代码放上来:
computed.ts
import { ReactiveEffect } from "./effect"
class computedImpl{
private _getter: any
private _dirty: boolean
private _effect: any
constructor(getter){
this._getter = getter
this._dirty = true
this._effect = new ReactiveEffect(getter,()=>{
if(!this._dirty) {
this._dirty = true
}
})
}
get value(){
if(this._dirty){
this._dirty = false
this._getter = this._effect.run()
return this._getter
}
}
}
export function computed(getter){
return new computedImpl(getter)
}
effect.ts的代码就不贴了,因为他就讲ReactiveEffect类导了出去而已,如图
那这样看下来,其实代码也就computed.ts中的20几行,接下来总结一下他的整体逻辑
首先,我们computed里面是有一个effect的;
然后,我们调用get value的时候就去调用我们effect.run,这里使用了一个dirty的变量来判断有没有被调用过(这里做的就是computed的缓存功能);
最后,我们通过scheduler来让他改变响应式对象的时候,却重置dirty变量