Vue3源码系列 (二) computed

930 阅读4分钟

想起上次面试,被问了个古老的问题:watch和computed的区别。多少有点感慨,现在已经很少见这种耳熟能详的问题了,网络上八股文不少。今天,我更想分享一下从源码的层面来区别这八竿子打不着的两者。上一篇看了watch的源码,本篇针对computed做分析。

一、类型声明

computed的源码在reactivity/src/computed.ts里,先来看看相关的类型定义:

  • ComputedRef:调用computed得到的值的类型,继承自WritableComputedRef
  • WritableComputedRef:继承自Ref,拓展了一个effect属性;
  • ComputedGetter:传递给ComputedRef的构造器函数,用于创建effect
  • ComputedSetter:传递给ComputedRef的构造器函数,用于在实例的值被更改时,即在set中调用;
  • WritableComputedOptions:可写的Computed选项,包含getset,是computed函数接收的参数类型之一。
declare const ComputedRefSymbol: unique symbol// ComputedRef的接口,调用computed()得到一个ComputedRef类型的值
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
  readonly value: T
  [ComputedRefSymbol]: true
}
​
// WritableComputedRef继承了Ref并拓展了一个只读属性effect
export interface WritableComputedRef<T> extends Ref<T> {
  readonly effect: ReactiveEffect<T>
}
​
// ComputedGetter 用于创建 effect , ComputedSetter 对应的值在 ComputedRef 实例中的 set 里调用
export type ComputedGetter<T> = (...args: any[]) => T
export type ComputedSetter<T> = (v: T) => void// 可写的Computed
export interface WritableComputedOptions<T> {
  get: ComputedGetter<T>
  set: ComputedSetter<T>
}

二、ComputedRef

computed()返回一个ComputedRef类型的值,那么这个ComputedRef就至关重要了。从接口声明中可以看出,它继承了Ref,因而其实现也和Ref较为相似:接收gettersetter等,用getter来创建effect,由effect.run()来获取value,在get中返回;而setter在实例的值更改时,即在set中调用。

export class ComputedRefImpl<T> {
  // dep: 收集的依赖
  public dep?: Dep = undefined
  
  // getter获取的实际值
  private _value!: T
  // 一个响应式的effect
  public readonly effect: ReactiveEffect<T>
  // __v_isRef 提供给 isRef() 判断实例是否为Ref
  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean = falsepublic _dirty = true
  // 是否可缓存
  public _cacheable: boolean
​
  // 构造器接收 getter 和 setter ,是否只读,是否出自 SSR
  constructor(
    getter: ComputedGetter<T>,
    // 接收只读的私有的 _setter 
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    // 用传入的 getter 创建一个 effect
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    // 把 effect 的 computed 属性指回 ComputedRef 实例自身
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }
​
  // 收集依赖,返回 this._value 的值
  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    // 收集Ref
    trackRefValue(self)
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      // effect.run() 会拿到 getter() 的值
      // 即_value的值来自于 effect,或者说来自于传入的 getter 的返回值
      self._value = self.effect.run()!
    }
    return self._value
  }
  
  // 当设置ComputedRef的实例的值时,调用传入的_setter
  set value(newValue: T) {
    this._setter(newValue)
  }
}

三、computed

1. computed的重载签名

computed有两个,主要是接收的第一个参数不同。一是类型为ComputedGetter的函数getter,该函数返回一个值;二是类型为WritableComputedOptions的**options,它是一个对象,包含getset两个函数,作用可以大致理解为与属性描述符里的getset相似**,但不是一回事,只是实现了相似的能力。事实上这个get的作用和第一种重载里的getter完全一致。换句话说,第一种重载没有set只有get,在后续的处理中,会给它包装一个set,只是包装的set只会触发警告。而第二种重载里自带set(由我们写代码时传入),除非我们传入的set是故意用于告警,否则是可以起作用的(通常在其中更新依赖数据的值,尤其是通过emit来告知父组件更新依赖数据)。

export function computed<T>(
  getter: ComputedGetter<T>,
  debugOptions?: DebuggerOptions
): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions
): WritableComputedRef<T>

2. computed的实现

  • 判断我们传入的第一个参数是getter还是options
  • 如果是getter,则包装一个setter用于开发环境下告警;
  • 如果是options,则取出其中的getset,分别作为gettersetter
  • gettersetter创建一个ComputedRef实例并返回该实例。
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
​
  // 判断是getter还是options
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
    // 包装setter
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
​
  // 创建并返回一个ComputedRef,
  // 第三个参数控制是否是只读的ComputedRef实例
  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里是不允许异步操作的,但是看完了computed的源码,好像也没发现哪里不允许异步操作。确实,单纯就computed的源码来看,它是允许异步操作的,但是computed作为计算属性,大致上是取getter的返回值,return是等不到异步操作结束的。而禁用异步操作的规定是在eslint-plugin-vue这个包中的lib/rules/no-async-in-computed-properties.js文件里的规定。

看完这两篇,下次如果还有人问watchcomputed的区别这种古董问题,就从源码上逐一比较吧。