实现computed
主要思路
computed()API 需要传入一个函数,这个函数我们在这里把它称为getter。在前面的reactivity模块中,其实也就是对象属性值所依赖的effect函数。
根据computed属性逐步实现,大致分为3个部分,每个部分都有对应的思路。
过程1: 基本实现
先简单实现一个 computed 函数,方法返回一个ComputedRefImpl 的实例。
class ComputedRefImpl {
private _getter: any
constructor(getter) {
this._getter = getter
}
get value() {
return this._getter()
}
}
export function computed(getter) {
return new ComputedRefImpl(getter)
}
实现缓存:当依赖的数据没有发生变化时,getter不会被调用
但是问题是,这里读取了两次 cValue.value, getter被调用了两次
因此接下来实现缓存的功能
过程2: 缓存
设置一个变量初始值为true(第一次访问数据的时候需要执行getter),当执行完getter操作时将变量置为false,封锁住getter的执行。【实际上,这个变量标志着当前的getter有没有被调用过】
class ComputedRefImpl {
private _getter: any
private _dirty: boolean = true
private _value: any
constructor(getter) {
this._getter = getter
}
get value() {
if (this._dirty) {
this._dirty = false
this._value = this._getter()
}
return this._value
}
}
export function computed(getter) {
return new ComputedRefImpl(getter)
}
那什么时候出发getter呢?
过程3: 当依赖的数据发生变化时,[并且! 用到了目标数据的时候], 触发getter操作
既然要实现数据发生变化触发对应的操作,这个过程再熟悉不过了,前面的effect.ts里的trigger函数不就是专门干这件事情的吗? 把它拿过来,再把getter函数传入就行了。
首先创建一个ReactiveEffect 实例,用_effect实例去搜集getter函数
import { trigger } from './effect'
import { ReactiveEffect } from './effect'
class ComputedRefImpl {
private _getter: any
private _dirty: boolean = true
private _value: any
private _effect: any
constructor(getter) {
this._getter = getter
this._effect = new ReactiveEffect(getter)
}
get value() {
if (this._dirty) {
this._dirty = false
this._value = this._effect.run()
}
return this._value
}
}
export function computed(getter) {
return new ComputedRefImpl(getter)
}
但还没有达到效果, getter 被执行了两次
为啥呢???? 这里迷惑了很久, 原因是,value.foo 这里的value本身是个reactive对象,当其发生更改的时候本来就会去触发对应的依赖函数一次,也就是getter。然后再加上前面的cValue.value 初始化的时候访问了一次,所以getter最开始就执行了一次。因此到这里的时候就执行了两次。
怎么把它变成只执行了一次呢? 也就是说,如何做到,只有当我们读取真正需要的值的时候,getter才会执行呢?
利用scheduler。
之前实现ReactiveEffect类的时候,我们有给实例添加一个scheduler属性,当我们在执行trigger的时候,我们可以选择是执行dep里的effect对象身上的run函数(也就是fn,也就是这里的getter函数),还是执行effect对象身上的scheduler方法。
在这里,我们选择执行scheduler方法。
为什么呢? 因为我们要保证,只有当访问我们需要的值(就是这里的cValue.value)的时候,我们才去执行getter函数。如果是直接执行run方法的话,那么当value.foo=2,value发生改变时,就会触发getter,我们并不希望这样。
所以添加一个scheduler方法就好啦!scheduler里面不需要去执行getter函数,并且我们还可以在scheduler里面,控制是否对当前的cValue 进行 读操作 (也就是 是否进入 get value()里面)。
添加代码之后,单测全部通过,但是还得想想为啥!再捋一捋
当响应式对象发生变化的时候,trigger执行的不是run()而是scheduler()。
======================================
认真想了一个晚上,终于又理清了一点逻辑。
这不得不回到之前的effect.ts 和 reactive.ts身上了。
首先可以明确的是,value.foo这个响应式对象关联的effect对象,是在computed.ts里面定义的这个_effect成员。因为我们想要触发value.foo的 scheduler() 而不是value.foo的run()
想了很久为什么 value.foo 关联的effect对象是定义在另一个ts文件里的?? 这就不得不回到track收集依赖的函数里去看了。
可以看到这里收集的是activeEffect对象,那么这个对象是啥?
可以看到activeEffect是一个全局变量,这个变量在effect实例执行run方法的时候被赋值。
被赋值成啥了?? 当然是run方法的调用者 ———当前effect实例了。
所以这个全局变量,是在effect实例第一次执行完run方法的之后,才会被赋值的。
当有了这个全局变量之后,我们对响应式对象进行proxy代理的时候,在触发get操作的时候,就可以通过track去收集这个当前这个响应式对象所依赖的effect实例也就是这个activeEffect全局变量了。
==============================================
接下来回到我们刚才的问题,value.foo 是怎么和定义在computed.ts里面的this._effect对象关联上的??
当然是执行了_effect.run()之后,初始化了全局对象activeEffect,然后在value.foo的get方法里收集到了这个全局对象activeEffect,之后在触发set操作的时候,就会去执行这个effect实例身上的scheduler方法啦!
关键还是在于这里。