Vue3 computed 源码学习

1,601 阅读5分钟

这是我参与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 函数中主要做了两件事,一个是定义 gettersetter,另一个就是创建 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 如果是 functioncomputed 是只读的;

如果 getterOrOptions 不是函数则将 getterOrOptions 中的 getset 赋值给 getterset 。这里就说明了在调用 computed 的时候可以传入一个 handler 对象,这也是 demo 中的第二种调用方式。

第二件事

return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
) as any

调用 ComputedRefImpl 类,并将 getter、setter 传入。第三个参数为是否只读,getterOrOptionsfunctiton 或者 getterOrOptions 中没有 set 属性都将是只读的。

接下来进入到 ComputedRefImpl

ComputedRefImpl 类

首先定义了几个属性

  • _value 私有属性,缓存计算之后的结果
  • _dirty 私有属性,标识是否需要重新计算
  • effect 只读属性,存放 reactiveEffect 函数
  • __v_isRef 只读属性,标识是不是 ref
  • __v_isReadonly 只读属性,标识是不是只读

随后在 constructor 中对 this.effectthis.__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._dirtytrue 的情况下才会调用 trigger 函数去触发依赖,也就是不需要计算的时候去触发依赖。

遇到的问题

  • : 在 ComputedRefImpl 中为什么要传入一个调度器

    : 用来控制当前计算属性所订阅的属性发生改变时,用来控制计算属性的调度时机

  • : 调用 this.effect 函数的时机是当 _dirtytrue 的时候,在 scheduler 调度器函数中是 _dirtyfalse 的时候,也就是不需要重新计算时才会调用 trigger 函数触发依赖。也就是说在调用 scheduler 函数的时候如果 this._dirtytrue 也不会触发依赖,那么如果当前调用 scheduler 的时候 this._dirtytrue,那什么时候会再次调呢?

    : 这个问题产生的原因是没有搞清楚整个收集和触发依赖的流程,没有搞清楚加 if (!this._dirty) {} 的原因。原因如下:

    • 当在获取 computed.value 之前多次更改 computed 中依赖的值的时候,每次更改就会触发这里的 scheduler
    • 第一次更改的时候 this._dirtyfalse,会进入到判断中,将 this._dirty 赋值为 true 执行 trigger
    • 第二次更改的时候就不需要再次进入判断了,因为设置 this._dirtytrue 的原因就是为了能够在获取 computed.value 的时候再次触发 this.effect 求值
    • 所以当第一次更改值将 this._dirty 设置为 true,后续就不需要再重复设置这个值了,后边无论更改多少次都无所谓。后边再次获取 computed.value 的时候只要 self._dirtytrue 就可以再次执行 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._dirtytrue 时才会调用 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 函数的作用是什么。

    : 这里的 trackscheduler 中调用的 trigger 都是为了解决有其他地方依赖了这个 computed 来服务的。

set

set value(newValue: T) {
    this._setter(newValue)
}

set 函数很简单,就是将外边传入的 setter 函数调用,并将 newValue 回传。

总结

  • computed 会进行缓存上一次计算出来的结果,当计算属性所依赖的属性没有发生改变时,访问计算属性会返回之前在 _value 缓存的值。
  • computed 的求值是惰性的,计算属性所依赖的属性发生变化的时候计算属性不会立刻去求值,而是调用了计算属性的 scheduler 函数将 _dirty 改为了 true 并调用 trigger 函数通知依赖这个计算属性的地方进行更新。只有触发了计算属性的 get value 函数的时候才会调用 this.effect 函数进行重新计算求值。

参考链接

Vue3源码解析(computed-计算属性)