vue computed 源码解析

lxf2023-05-05 07:51:01

vue computed 源码解析

引入:和 ref 的对比

如果你已经对ref的源码有了一定的了解,那么computed的源码看起来是很简单的,其逻辑和ref的逻辑大致相同,只在一些地方存在区别:

  • refgetter中收集依赖,在setter中触发依赖;而computed一般是没有setter的,如果用户在定义计算属性时给定了settercomputedsetter中也只是简单的执行用户传入的setter,而不会显式地触发依赖

    export class ComputedRefImpl<T> {
      /* 省略其他代码 */
      
      set value(newValue: T) {
        this._setter(newValue)
      }
    }
    
  • computed对象既是一种ref,也是副作用。和ref相同,通过value属性可以访问计算属性的值;和副作用相同,计算属性有effect属性,当计算属性的依赖发生改变时,它会被触发:

    // 初始化计算属性的 effect 属性,参数分别为 fn 和 调度器 scheduler
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    

    但是computed区别于一般副作用的一点是,当一般副作用的依赖发生改变时,这个副作用会被重新调用;而当computed的依赖改变时,它的getter并不会被调用,而是触发依赖该计算属性的副作用,让它们重新执行,只有当副作用访问到该计算属性时,才会更新计算属性的值

正文

计算属性的创建

计算属性的创建流程的第一步是调用computed函数,该函数会处理用户的传入,并使用这些参数实例ComputedRefImpl类对象

逻辑很容易看懂,就是拿到用户传入的gettersetter,如果没有setter则指定警告或空函数,将这些传入构造函数得到cRef并返回

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 = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
​
  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
}

计算属性创建的第二步是在构造函数中设置this.effect,其中第一个参数getter会在effect.run中被调用,第二个参数是副作用的调度器,当响应式变量更改,从而更新计算属性时,调度器会在triggerEffect函数中被执行

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
  }
}

计算属性的更新过程

那么,当计算属性的依赖发生改变时,计算属性的值是如何更新的,又是怎样触发依赖它的副作用的呢?

事实上,依赖收集的过程和ref是相同的,都是在getter中通过trackRefValue收集依赖。

而依赖触发的过程则完全不同,当计算属性的依赖发生改变时,会触发它的调度器,这首先会设置计算属性的_dirty属性,表示该计算属性是“脏”的,即它的值需要更新;接着会调用triggerRefValue触发计算属性的依赖,在它的依赖被调用的时候,会访问到计算属性的值,从而会触发getter

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中,如果发现值是需要更新的,则会调用effect.run,从而重新调用用户传入的getter,更新自身的值

setter 相关逻辑

computedsetter中,我们一般是更新该计算属性的依赖,例如

const val = ref(1)
const cVal = computed({
  get() {
    return val.value * 2
  }
  
  set(newValue) {
    val.value = newValue / 2
  }
})

在这个setter中,我们更新了val,由于cVal是该变量的依赖,所以会被触发,从而会更新它的值。这也就是为什么源码在setter中只是调用用户传入的setter而没有触发依赖