Vue3_Composition API - computed实现原理

1,551 阅读4分钟

之前关于Vue3的整体运行过程做了简单的原理分析,在实际开发过程中使用Vue3API也是必不可少的,接下来会对Vue3的一下常用的Composition API的实现原理简易分析

computed的使用

import { computed, reactive } from 'vue'

// 传参是函数
const double = computed(() => num.count * 2)

// 传参是对象,包含set、get钩子函数
const double = computed({
  get: () => {
    console.log('get double')
    return num.count * 2
  },
  set: (val) => (num.count = val / 2)
})

double.value = 20 // 直接修改double的值

当参数为函数时,计算属性double的值随着num.count的值的变化而变化,但是计算属性的值不可直接修改;当参数为对象时,计算属性的值可读可写。

computed API内部实现

首先当执行setup函数时,遇到computed函数开始执行

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions
) {
  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)

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

  return cRef as any
}

校验参数

computed函数内部首先校验入参getterOrOptions是否是一个函数,如果是函数,则会设置setter() => console.warn(),当修改计算属性的值时会报错;如果是一个对象,也只支持对象的属性为set、get

ComputedRefImpl实例对象

之后创建ComputedRefImpl实例对象,参数为setget 和 是否是只读属性的标识位。接着看下ComputedRefImpl类的内部实现

// ComputedRefImpl 类
public dep?: Dep = undefined // 依赖
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean

// constructor
constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    this.effect = new ReactiveEffect(getter, )
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

首先给ComputedRefImpl类的effect属性设置为ReactiveEffect类的实例对象,传参为getter钩子函数 和 自定义的调度函数scheduler。之后定义value属性的getset拦截钩子函数。从这里可以看出使用计算属性的时候也是依赖于它的value属性(template模版中可直接使用,因为计算属性的__v_isRef=true,参考ref函数的使用与实现)

触发计算属性的get函数

setup解析完成之后,就是模版编译生成render函数,之后执行render函数,当遇到计算属性时,会获取它的值(类似于获取ref返回的数据值)。触发get钩子函数的执行

get value() {
    const self = toRaw(this)
    trackRefValue(self)
    if (self._dirty) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

get钩子函数中首先是trackRefValue方法进行依赖收集,大致实现是在ComputedRefImpl实例对象的dep属性中添加全局的依赖activeEffect

因为在模版编译之前会创建ReactiveEffect实例对象,参数的fncomponentUpdateFn(render+patch),componentUpdateFn方法的执行是因为执行了ReactiveEffect实例对象中的run方法,run方法中会将activeEffect全局依赖赋值当前的ReactiveEffect对象。所以在ComputedRefImplvalueget钩子函数执行时,activeEffect全局依赖为ReactiveEffect实例对象(render + patch)

这样计算属性的dep中收集的是(render+patch)的依赖。

get钩子函数中后续执行了self.effect.run()!effect属性是初始化computed对象时创建的ReactiveEffect实例对象,执行run方法,首先将全局的activeEffect依赖设置为计算属性的ReactiveEffect对象,然后执行传入的fn,即调用computed方法时传入的get函数(() => num.count * 2)。

当获取num.count时,因为numProxy代理对象,所以会触发Proxy拦截器的get钩子函数,进行num的依赖收集,此时是将全局依赖activeEffect收集在了numdep属性中。

依赖数据发生变化

当依赖的num数据发生变化时会触发依赖的执行,也就是计算属性创建的ReactiveEffect实例对象,执行传入的调度函数

// 计算属性传入的调度函数scheduler
() => {
  if (!this._dirty) {
    this._dirty = true
    triggerRefValue(this)
  }
}

在计算属性的调度函数中又会执行自己收集的依赖,而计算属性收集的依赖是模版编译后创建的ReactiveEffect实例对象(render+patch),此时执行调度函数则是重新执行renderpatch方法,更新页面渲染。

setter钩子函数

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

ComputedRefImpl实例对象的setter钩子函数,就是执行传入的set属性函数,所以当入参是对象是,修改计算属性的值会相应的执行传入的set函数。

总结

首先computed执行时会创建ReactiveEffect实例对象,传参为传入的get和自定义的调度函数scheduler。然后设置value属性的getset拦截函数。当获取计算属性时触发get函数的执行,会收集render+patch的依赖,然后执行计算属性值的计算过程,当获取依赖数据的值时会触发依赖数据的get钩子函数执行,收集依赖,收集的是计算属性的ReactiveEffect对象。

当依赖数据发生变化时,首先会触发依赖数据的依赖执行,就是计算属性中自定义的调度函数,执行中又会执行计算属性的依赖,就是renderpatch方法的执行,从而更新渲染。