Vue3 源码解析系列 - Computed

380 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情

前言

Computed 方法有两种用法,一个是传入一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象;另外一个是接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。

使用

只传入 getter,生成的 ref 对象 refCount 为只读。

import { ref, computed } from 'vue'
const count = ref(1)
const refCount = computed(() => count.value + 1)

console.log(refCount.value) // 2

refCount.value++ // 错误

接受一个具有 get 和 set 函数的对象,赋值时触发 set

import { ref, computed } from 'vue'
const count = ref(1)
const refCount = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

refCount.value = 1
console.log(count.value) // 0

源码

computed 方法的位置在 packages/reactivity/src/computed.ts

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
    setter = NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)

  return cRef as any
}
  1. 判断所传的参数是否是函数,如果是,说明传进去参数为 getter,而且没有 setter
  2. 如果是对象,那么把对象的 get 和 set 分别传给 getter 和 setter
  3. 把getter 和 setter传给 ComputedRefImpl 类,实例化出ref对象
  4. 最后返回 ref 对象

ComputedRefImpl 类

我们先看下这个类的成员变量

export class ComputedRefImpl<T> {
  public dep?: Dep = undefined

  private _value!: T
  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  public _dirty = true
  public _cacheable: boolean
}
  • 私有变量 _value 保存数据
  • dep 保存依赖
  • effect 一个只读的副作用函数
  • 在调度器中判断 _dirty 是否为 false,如果是的话,会利用 trigger 派发更新。

再来看下这个类的构造函数

export class ComputedRefImpl<T> {
  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }
}

创建一个 effect 对象,传入getter 和 一个调度器,这个调度器,会在使用 triggerEffect 进行派发更新时调用,这个调度器判断 _dirty 是否为false, 是的话,把 _dirty 恢复成 true,并进行派发更新,这个 triggerRefValue 最终会执行 triggerEffect。

然后把当前对象赋值给 this.effect

export class ComputedRefImpl<T> {
  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    trackRefValue(self)
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

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

通过 getter 函数获取值时,会先执行副作用函数,并将副作用函数的返回值赋值给 _value,并将 _dirty 的值赋值给 false,这就可以保证如果 computed 中的依赖没有发生变化,则副作用函数不会再次执行,那么在 getter 时获取到的 _dirty 始终是 false,也不需要再次执行副作用函数,节约开销。之后通过 track 收集依赖,并返回 _value 的值。

在 setter 中直接执行我们传入的 set 函数。

总结

总的来说,核心依然是通过 effect 进行依赖收集和派发更新,区别就是在 getter 和 setter 是调用了用户传进去的方法。