Vue3 computed实现
紫圣 人气:0版本:3.2.31
computed 的函数签名
// packages/reactivity/src/computed.ts // 只读的 export function computed<T>( getter: ComputedGetter<T>, debugOptions?: DebuggerOptions ): ComputedRef<T> // 可写的 export function computed<T>( options: WritableComputedOptions<T>, debugOptions?: DebuggerOptions ): WritableComputedRef<T> export function computed<T>( getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>, debugOptions?: DebuggerOptions, isSSR = false )
上面的代码为 computed 的函数重载。在第一个重载中,接受一个 getter 函数,并返回 ComputedRef 类型的值。也就是说,在这种情况下,computed 接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。
如下面的代码所示:
const count = ref(1) // computed 接受一个 getter 函数 const plusOne = computed(() => count.value + 1) console.log(plusOne.value) // 2 plusOne.value++ // 错误
在第二个重载中,computed 函数接受一个具有 get 和 set 函数的 options 对象,并返回一个可写的 ref 对象。
如下面的代码所示:
const count = ref(1) const plusOne = computed({ // computed 函数接受一个具有 get 和 set 函数的 options 对象 get: () => count.value + 1, set: val => { count.value = val - 1 } }) plusOne.value = 1 console.log(count.value) // 0
第三个重载是第一个重载和第二个重载的结合,此时 computed 函数既可以接受一个 getter 函数,又可以接受一个具有 get 和 set 函数的 options 对象。
computed 的实现
// packages/reactivity/src/computed.ts export function computed<T>( getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>, debugOptions?: DebuggerOptions, isSSR = false ) { let getter: ComputedGetter<T> let setter: ComputedSetter<T> // 判断 getterOrOptions 参数 是否是一个函数 const onlyGetter = isFunction(getterOrOptions) if (onlyGetter) { // getterOrOptions 是一个函数,则将函数赋值给取值函数getter getter = getterOrOptions setter = __DEV__ ? () => { console.warn('Write operation failed: computed value is readonly') } : NOOP } else { // getterOrOptions 是一个 options 选项对象,分别取 get/set 赋值给取值函数getter和赋值函数setter getter = getterOrOptions.get setter = getterOrOptions.set } // 实例化一个 computed 实例 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 函数的实现中,首先判断传入的 getterOrOptions 参数是 getter 函数还是 options 对象。
如果 getterOrOptions 是 getter 函数,则直接将传入的参数赋值给 computed 的 getter 函数。由于这种情况下的计算属性是只读的,因此不允许设置 setter 函数,并且在 DEV 环境中设置 setter 会报出警告。
如果 getterOrOptions 是 options 对象,则将该对象中的 get 、set 函数分别赋值给 computed 的 gettter 和 setter。
处理完 computed 的 getter 和 setter 后,则根据 getter 和 setter 创建一个 ComputedRefImpl 类的实例,该实例是一个 ref 对象,最后将该 ref 对象返回。
下面我们来看看 ComputedRefImpl 这个类。
ComputedRefImpl 类
// packages/reactivity/src/computed.ts export class ComputedRefImpl<T> { public dep?: Dep = undefined // value 用来缓存上一次计算的值 private _value!: T public readonly effect: ReactiveEffect<T> public readonly __v_isRef = true public readonly [ReactiveFlags.IS_READONLY]: boolean // dirty标志,用来表示是否需要重新计算值,为true 则意味着 脏, 需要计算 public _dirty = true public _cacheable: boolean constructor( getter: ComputedGetter<T>, private readonly _setter: ComputedSetter<T>, isReadonly: boolean, isSSR: boolean ) { this.effect = new ReactiveEffect(getter, () => { // getter的时候,不派发通知 if (!this._dirty) { this._dirty = true // 当计算属性依赖响应式数据变化时,手动调用 triggerRefValue 函数 触发响应式 triggerRefValue(this) } }) this.effect.computed = this this.effect.active = this._cacheable = !isSSR this[ReactiveFlags.IS_READONLY] = isReadonly } get value() { // the computed ref may get wrapped by other proxies e.g. readonly() #3376 // 获取原始对象 const self = toRaw(this) // 当读取 value 时,手动调用 trackRefValue 函数进行追踪 trackRefValue(self) // 只有脏 才计算值,并将得到的值缓存到value中 if (self._dirty || !self._cacheable) { // 将dirty设置为 false, 下一次访问直接使用缓存的 value中的值 self._dirty = false self._value = self.effect.run()! } // 返回最新的值 return self._value } set value(newValue: T) { this._setter(newValue) } }
缓存计算属性,避免多次计算:
为了避免多次访问计算属性时导致副作用函数多次执行,在 ComputedRefImpl 类中定义了一个私有变量 _value 和一个公共变量 _dirty。其中 _value 用来缓存上一次计算的值,_dirty 用来表示是否需要重新计算值,值为 true 时意味着「脏」, 则计算属性需要重新计算。在读取计算属性时,会触发 getter 函数,在 getter 函数中,判断 _dirty 的值是否为 true,如果是,才重新执行副作用,将执行结果缓存到 _value 变量中,并返回最新的值。如果_dirty 的值为 false,说明计算属性不需要重新计算,返回上一次计算的结果即可。
数据变化,计算属性需重新计算:
当计算属性的依赖数据发生变化时,为了使得计算属性是最新的,Vue 在 ComputedRefImpl 类的构造函数中为 getter 创建了一个副作用函数。在该副作用函数中,判断 this._dirty 标记是否为 false,如果是,则将 this._dirty 置为 true,当下一次访问计算属性时,就会重新执行副作用函数计算值。
计算属性中的 effect 嵌套:
当我们在另一个 effect 中读取计算属性的值时,如下面代码所示:
const sumResult = computed(() => obj.foo + obj.bar) effect(() => { // 在该副作用函数中读取 sumResult.value console.log(sumResult.value) }) // 修改 obj.bar 的值 obj.bar++
如上面的代码所示,sumResult 是一个计算属性,并且在另一个 effect 的副作用函数中读取了 sumResult.value 的值。如果此时修改了 obj.bar 的值,期望的结果是副作用函数重新执行,但实际上并未重新触发副作用函数执行。
在一个 effect 中读取计算属性的值,其本质上就是一个典型的 effect 嵌套。一个计算属性内部拥有自己的 effect ,并且它是懒执行的,只有当真正读取计算属性的值时才会执行。当把计算属性用于另外一个 effect 时,就会发生 effect 嵌套,外层的 effect 不会被内层 effect 中的响应式数据收集。因此,当读取计算属性的值时,需要手动调用 trackRefValue 函数进行追踪,当计算属性依赖的响应式数据发生变化时,手动调用 triggerRefValue 函数触发响应。
总结
computed 的实现,它实际上就是一个懒执行的副作用函数,通过 _dirty 标志使得副作用函数可以懒执行。dirty 标志用来表示是否需要重新计算值,当值为 true 时意味着「脏」, 则计算属性需要重新计算,即重新执行副作用。
加载全部内容