Vue3 源码解读之计算属性computed的实现原理

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情 >>

版本:3.2.31

computed 的函数签名

// packages/reactivity/src/computed.ts

// 只读的
export function computed<T>(
  getter: ComputedGetter<T>,
  debugOptions?: DebuggerOptions
): ComputedRef<T>
// 可写的 
export function computed<T>(
  options: WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
)
复制代码

上面的代码为 computed 的函数重载。在第一个重载中,接受一个 getter 函数,并返回 ComputedRef 类型的值。也就是说,在这种情况下,computed 接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。如下面的代码所示:

const count = ref(1)
// computed 接受一个 getter 函数
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误
复制代码

在第二个重载中,computed 函数接受一个具有 get 和 set 函数的 options 对象,并返回一个可写的 ref 对象。如下面的代码所示:

const count = ref(1)
const plusOne = computed({
  // computed 函数接受一个具有 get 和 set 函数的 options 对象
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0
复制代码

第三个重载是第一个重载和第二个重载的结合,此时 computed 函数既可以接受一个 getter 函数,又可以接受一个具有 get 和 set 函数的 options 对象。

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>

  // 判断 getterOrOptions 参数 是否是一个函数
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    // getterOrOptions 是一个函数,则将函数赋值给取值函数getter 
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    // getterOrOptions 是一个 options 选项对象,分别取 get/set 赋值给取值函数getter和赋值函数setter
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  // 实例化一个 computed 实例
  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)

  if (__DEV__ && debugOptions && !isSSR) {
    cRef.effect.onTrack = debugOptions.onTrack
    cRef.effect.onTrigger = debugOptions.onTrigger
  }

  return cRef as any
}
复制代码

在 computed 函数的实现中,首先判断传入的 getterOrOptions 参数是 getter 函数还是 options 对象。

如果 getterOrOptions 是 getter 函数,则直接将传入的参数赋值给 computed 的 getter 函数。由于这种情况下的计算属性是只读的,因此不允许设置 setter 函数,并且在 DEV 环境中设置 setter 会报出警告。

如果 getterOrOptions 是 options 对象,则将该对象中的 get 、set 函数分别赋值给 computed 的 gettter 和 setter。

处理完 computed 的 getter 和 setter 后,则根据 getter 和 setter 创建一个 ComputedRefImpl 类的实例,该实例是一个 ref 对象,最后将该 ref 对象返回。

下面我们来看看 ComputedRefImpl 这个类。

ComputedRefImpl 类

// packages/reactivity/src/computed.ts

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

  // value 用来缓存上一次计算的值
  private _value!: T
  public readonly effect: ReactiveEffect<T>

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

  // dirty标志,用来表示是否需要重新计算值,为true 则意味着 脏, 需要计算
  public _dirty = true
  public _cacheable: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      // getter的时候,不派发通知
      if (!this._dirty) {
        this._dirty = true
        // 当计算属性依赖响应式数据变化时,手动调用 triggerRefValue 函数 触发响应式
        triggerRefValue(this)
      }
    })
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    // 获取原始对象
    const self = toRaw(this)
    // 当读取 value 时,手动调用 trackRefValue 函数进行追踪
    trackRefValue(self)
    // 只有脏 才计算值,并将得到的值缓存到value中
    if (self._dirty || !self._cacheable) {
      // 将dirty设置为 false, 下一次访问直接使用缓存的 value中的值
      self._dirty = false
      self._value = self.effect.run()!
    }
    // 返回最新的值
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}
复制代码
  • 缓存计算属性,避免多次计算

为了避免多次访问计算属性时导致副作用函数多次执行,在 ComputedRefImpl 类中定义了一个私有变量 _value 和一个公共变量 _dirty。其中 _value 用来缓存上一次计算的值,_dirty 用来表示是否需要重新计算值,值为 true 时意味着「脏」, 则计算属性需要重新计算。在读取计算属性时,会触发 getter 函数,在 getter 函数中,判断 _dirty 的值是否为 true,如果是,才重新执行副作用,将执行结果缓存到 _value 变量中,并返回最新的值。如果_dirty 的值为 false,说明计算属性不需要重新计算,返回上一次计算的结果即可。

  • 数据变化,计算属性需重新计算

当计算属性的依赖数据发生变化时,为了使得计算属性是最新的,Vue 在 ComputedRefImpl 类的构造函数中为 getter 创建了一个副作用函数。在该副作用函数中,判断 this._dirty 标记是否为 false,如果是,则将 this._dirty 置为 true,当下一次访问计算属性时,就会重新执行副作用函数计算值。

  • 计算属性中的 effect 嵌套

当我们在另一个 effect 中读取计算属性的值时,如下面代码所示:

const sumResult = computed(() => obj.foo + obj.bar)

effect(() => {
  // 在该副作用函数中读取 sumResult.value
  console.log(sumResult.value)
})

// 修改 obj.bar 的值
obj.bar++
复制代码

如上面的代码所示,sumResult 是一个计算属性,并且在另一个 effect 的副作用函数中读取了 sumResult.value 的值。如果此时修改了 obj.bar 的值,期望的结果是副作用函数重新执行,但实际上并未重新触发副作用函数执行。

在一个 effect 中读取计算属性的值,其本质上就是一个典型的 effect 嵌套。一个计算属性内部拥有自己的 effect ,并且它是懒执行的,只有当真正读取计算属性的值时才会执行。当把计算属性用于另外一个 effect 时,就会发生 effect 嵌套,外层的 effect 不会被内层 effect 中的响应式数据收集。因此,当读取计算属性的值时,需要手动调用 trackRefValue 函数进行追踪,当计算属性依赖的响应式数据发生变化时,手动调用 triggerRefValue 函数触发响应。

总结

本文介绍了computed 的实现,它实际上就是一个懒执行的副作用函数,通过 _dirty 标志使得副作用函数可以懒执行。dirty 标志用来表示是否需要重新计算值,当值为 true 时意味着「脏」, 则计算属性需要重新计算,即重新执行副作用。

猜你喜欢

转载自juejin.im/post/7129278309359157262