【vue3源码】四、computed源码解析

参考代码版本:vue 3.2.37

官网文档:https://vuejs.org/

计算属性。承受一个getter函数,并依据getter函数的返回值返回一个不可变的响应式ref对象。或者,承受一个具备getset函数的对象,用来创立可写的ref对象。
文件地位:packages/reactivity/src/computed.ts

应用示例

只读的计算属性:

const count = ref(1)const doubleCount = computed(() => count.value * 2)console.log(doubleCount.value) // 2count.value = 2console.log(doubleCount.value) // 3

可写的计算属性:

const count = ref(1)const plusOne = computed({  get: () => count.value + 1,  set: (val) => {    count.value = val - 1  }})console.log(plusOne.value) // 2plusOne.value = 1console.log(count.value) // 0

进行调试:

const count = ref(1)const doubleCount = computed(() => count.value * 2, {  onTrack(e) {    console.log('track')    console.log(e)  },  onTrigger(e) {    console.log('trigger')    console.log(e)  }})// 触发track监听console.log(doubleCount.value)// 触发trigger监听count.value++

源码

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}

computed接管两个参数,第二参数是依赖收集和触发依赖的钩子函数,只在开发环境中起作用,这里就不做解释了。次要看第一个参数,察看其类型,发现能够传两种参数:一种是一个getter函数,一种是个蕴含getset的对象。

首先从getterOrOptions中确定gettersetter(如果getterOrOptions是个function,阐明computed是不可写的,所以会将setter设置为一个空函数),确定好之后,创立一个ComputedRefImpl实例,并将其返回。

ComputedRefImpl

export class ComputedRefImpl<T> {  public dep?: Dep = undefined  // 缓存的值  private _value!: T  // 在结构器中创立的ReactiveEffect实例  public readonly effect: ReactiveEffect<T>  // 标记为一个ref类型  public readonly __v_isRef = true  // 只读标识  public readonly [ReactiveFlags.IS_READONLY]: boolean  // 是否为脏数据,如果是脏数据须要从新计算  public _dirty = true  // 是否可缓存,取决于SSR  public _cacheable: boolean  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  }  get value() {    // computed可能被其余proxy包裹,如readonly(computed(() => foo.bar)),所以要获取this的原始对象    const self = toRaw(this)    // 收集依赖    trackRefValue(self)    // 如果是脏数据或者是SSR,须要从新计算    if (self._dirty || !self._cacheable) {      // _dirty取false,避免依赖不变反复计算      self._dirty = false      // 计算      self._value = self.effect.run()!    }    return self._value  }  set value(newValue: T) {    this._setter(newValue)  }}
结构器

ComputedRefImpl结构器接管四个参数:gettersetterisReadonly(是否只读)、isSSR(是否为SSR)。

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.computed = this  // this.effect.active与this._cacheable在SSR中为false  this.effect.active = this._cacheable = !isSSR  this[ReactiveFlags.IS_READONLY] = isReadonly}

在结构器中申明了一个ReactiveEffect,并将getter和一个调度函数作为参数传入,在调度器中如果_dirtyfalse,会将_dirty设置为true,并执行triggerRefValue函数。

triggerRefValue能够承受两个值:refnewVal

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {  ref = toRaw(ref)  if (ref.dep) {    if (__DEV__) {      triggerEffects(ref.dep, {        target: ref,        type: TriggerOpTypes.SET,        key: 'value',        newValue: newVal      })    } else {      triggerEffects(ref.dep)    }  }}

triggerRefValue中首先获取ref的原始对象,如果ref的原始对象中有dep属性,则触发dep中的依赖。

在初始化effect之后,会将这个effect赋给ComputedRefImpl实例的effect属性,并将effect.computed指向ComputedRefImpl实例

value取值函数
get value() {  // computed可能被其余proxy包裹,如readonly(computed(() => foo.bar)),所以要获取this的原始对象  const self = toRaw(this)  // 收集依赖  trackRefValue(self)  // 如果是脏数据,须要从新计算  if (self._dirty || !self._cacheable) {    // _dirty取false,避免依赖不变反复计算    self._dirty = false    // 计算    self._value = self.effect.run()!  }  return self._value}

当读取ComputedRefImpl实例的value属性时,因为计算属性可能被其余proxy包裹,所以须要应用toRaw获取其原始对象。

const self = toRaw(this)

而后调用trackRefValue进行依赖的收集。

export function trackRefValue(ref: RefBase<any>) {  // 如果容许收集并且存在activeEffect进行依赖收集  if (shouldTrack && activeEffect) {    ref = toRaw(ref)    if (__DEV__) {      trackEffects(ref.dep || (ref.dep = createDep()), {        target: ref,        type: TrackOpTypes.GET,        key: 'value'      })    } else {      trackEffects(ref.dep || (ref.dep = createDep()))    }  }}

接着依据_dirty_cacheable属性来决定是否须要批改self._value,其中_dirty示意是否为脏数据,_cacheable示意是否能够缓存(取决于是否为服务端渲染,如果为服务端渲染则不能够缓存)。如果是脏数据或不能够被缓存,那么会将_dirty设置为false,并调用self.effect.run(),批改self._value

if (self._dirty || !self._cacheable) {  self._dirty = false  self._value = self.effect.run()!}

最初返回self._value

return self._value
value存值函数
set value(newValue: T) {  this._setter(newValue)}

当批改ComputedRefImpl实例的value属性时,会调用实例的_setter函数。

到此,你会发现computed是懈怠的,只有应用到computed的返回后果,能力触发相干计算。

为了加深对computed的了解,接下来以一个例子剖析computed的缓存及计算过程:

const value = reactive({ foo: 1 })const cValue = computed(() => value.foo)console.log(cValue.value) // 1value.foo = 2console.log(cValue.value) // 2

当打印cValue.value时,会命中ComputedRefImpl对应的get办法,在get中,执行trackRefValue收集对应依赖(因为此时没有处于沉闷状态的effect,即activeEffect,所以并不会进行依赖的收集),默认_dirtytrue,将_dirty设置为false,并执行effect.run,计算数据,计算实现后将数据缓存至selft._vlaue中,不便下次的利用。在调用effect.run过程中,会将在ComputedRefImpl结构器中创立的ReactiveEffect实例收集到targetMap[toRaw(value)].foo中。

当批改value.foo = 2,触发targetMap[toRaw(value)].foo中的依赖,因为在初始化ReactiveEffect时,设置了一个调度器,所以在触发依赖过程中会执行这个调度器。这个调度器中会判断如果_dirty===false,则将_dirty设置为true,并手动调用triggerRefValue触发依赖,在调用triggerRefValue的过程中,因为cValue.dep=undefined,所以没有依赖要触发。

当第二次打印cValue.value时,因为_dirtytrue,所以会执行cValue.effect.run,并将后果赋值给cValue._value,最初返回cValue._value,打印后果2

总结

computed实质也是个refComputedRefImpl),它是懈怠的,如果不应用计算属性,那么是不会进行计算的,只有应用它,才会调用计算属性中的effect.run办法进行计算,同时将后果缓存到_value中。

computed如何从新计算?

首先在第一次获取计算属性的值的过程中会进行依赖的收集,假如计算属性的计算与响应式对象的a、b两个属性无关,那么会将computed中生成的ReactiveEffect实例收集到targetMap[obj].atargetMap[obj].b中,一旦ab属性变动了,会触发依赖,而在依赖的触发过程中会执行调度函数,在调度函数中会将脏数据的标识_dirty设置为true,并触发计算属性的依赖。那么在下一次应用到计算属性的话,因为_dirtytrue,便会调用计算属性中的effect.run办法从新计算值。