这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
前言
照旧是准备一个 demo 从 debug 开始
...
const data = reactive({
a: 1,
b: 2,
count: 0
})
const computedVal = computed(() => {
return data.a + data.b
})
console.log('computedVal =>', computedVal.value)
const computedVal2 = computed({
get: () => {
return data.a + data.b
},
set: (value) => {
data.a = value
}
})
console.log('computedVal2 =>', computedVal2.value)
console.log('第二次求值 =>', computedVal2.value)
...
开始吧
在 computed 函数中主要做了两件事,一个是定义 getter 和 setter,另一个就是创建 ComputedRefImpl 实例并 return。
先来看第一个
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
代码中先判断了传入的 getterOrOptions 参数是不是一个 function 是的话则将这个 function 赋值给 getter,同时如果是 function setter 被定义为了一个空函数。也就是说 getterOrOptions 如果是 function 则 computed 是只读的;
如果 getterOrOptions 不是函数则将 getterOrOptions 中的 get 和 set 赋值给 getter 和 set 。这里就说明了在调用 computed 的时候可以传入一个 handler 对象,这也是 demo 中的第二种调用方式。
第二件事
return new ComputedRefImpl(
getter,
setter,
isFunction(getterOrOptions) || !getterOrOptions.set
) as any
调用 ComputedRefImpl 类,并将 getter、setter 传入。第三个参数为是否只读,getterOrOptions 为 functiton 或者 getterOrOptions 中没有 set 属性都将是只读的。
接下来进入到 ComputedRefImpl 中
ComputedRefImpl 类
首先定义了几个属性
_value私有属性,缓存计算之后的结果_dirty私有属性,标识是否需要重新计算effect只读属性,存放reactiveEffect函数__v_isRef只读属性,标识是不是ref__v_isReadonly只读属性,标识是不是只读
随后在 constructor 中对 this.effect 和 this.__v_isReadonly 进行了赋值,this.effect 对应的就是 effect 函数的返回值。
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value')
}
}
})
this[ReactiveFlags.IS_READONLY] = isReadonly
这个
effect函数在 Vue3 Reactive 源码学习 文章中提到过,他的作用就是将传入的function进行包装,包装成为一个reactiveEffect函数,也就是我们最终要收集的依赖,在调用effect函数的过程中,会将activeEffect进行赋值,也就可以保证在调用track函数的时候可以正确的收集依赖。
在调用 effect 函数的时候需要注意的是在 scheduler 调度器方法中判断了 !this._dirty 为 true 的情况下才会调用 trigger 函数去触发依赖,也就是不需要计算的时候去触发依赖。
遇到的问题
-
问: 在
ComputedRefImpl中为什么要传入一个调度器答: 用来控制当前计算属性所订阅的属性发生改变时,用来控制计算属性的调度时机
-
问: 调用
this.effect函数的时机是当_dirty为true的时候,在scheduler调度器函数中是_dirty为false的时候,也就是不需要重新计算时才会调用trigger函数触发依赖。也就是说在调用scheduler函数的时候如果this._dirty为true也不会触发依赖,那么如果当前调用scheduler的时候this._dirty为true,那什么时候会再次调呢?答: 这个问题产生的原因是没有搞清楚整个收集和触发依赖的流程,没有搞清楚加
if (!this._dirty) {}的原因。原因如下:- 当在获取
computed.value之前多次更改computed中依赖的值的时候,每次更改就会触发这里的scheduler, - 第一次更改的时候
this._dirty为false,会进入到判断中,将this._dirty赋值为true执行trigger - 第二次更改的时候就不需要再次进入判断了,因为设置
this._dirty为true的原因就是为了能够在获取computed.value的时候再次触发this.effect求值 - 所以当第一次更改值将
this._dirty设置为true,后续就不需要再重复设置这个值了,后边无论更改多少次都无所谓。后边再次获取computed.value的时候只要self._dirty为true就可以再次执行this.effect求值计算出最新的computed。
- 当在获取
接下来就要去看 get 函数了。
get
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
if (self._dirty) {
self._value = this.effect()
self._dirty = false
}
track(self, TrackOpTypes.GET, 'value')
return self._value
}
在 get 函数中,self._dirty 为 true 时才会调用 this.effect 函数,随后将 self._dirty 设置为 false。在条件外调用了 track 函数来收集依赖,最后将计算后的结果 return。
track函数在 Vue3 Reactive 源码学习 文章中提到过。
遇到的问题
-
问:
_dirty是干什么用,get的这个判断是干啥用的const self = toRaw(this) if (self._dirty) { self._value = this.effect() self._dirty = false }答: 控制计算属性是否需要重新计算,在第一次计算之后这个值就会变成
false,并将值缓存起来。当计算属性内所依赖的属性没有发生变化的条件下再次获取计算属性的值,这个时候_dirty的值为false则会直接return _value将上一次缓存的值返回。 -
问:
computed会收集用到的属性的依赖,get函数中的track是不是来完成这个操作的?但是根据已有的 demo,执行到track函数中会被return,因为activeEffect === undefined,那么这里的调用track函数的作用是什么。答: 这里的
track和scheduler中调用的trigger都是为了解决有其他地方依赖了这个computed来服务的。
set
set value(newValue: T) {
this._setter(newValue)
}
set 函数很简单,就是将外边传入的 setter 函数调用,并将 newValue 回传。
总结
computed会进行缓存上一次计算出来的结果,当计算属性所依赖的属性没有发生改变时,访问计算属性会返回之前在_value缓存的值。computed的求值是惰性的,计算属性所依赖的属性发生变化的时候计算属性不会立刻去求值,而是调用了计算属性的scheduler函数将_dirty改为了true并调用trigger函数通知依赖这个计算属性的地方进行更新。只有触发了计算属性的get value函数的时候才会调用this.effect函数进行重新计算求值。