乐趣区

关于前端:vue3源码四computed源码解析

【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) // 2

count.value = 2
console.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) // 2

plusOne.value = 1
console.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) // 1

value.foo = 2
console.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 办法从新计算值。

退出移动版