一、简要介绍
不论是计算属性,还是异步计算属性,都是依托于Vue3整体的响应式原理实现的。其核心依旧是ReacetEffect类。如果对响应式原理不清楚,建议先看 响应式原理 章节。
计算属性和常规的动态响应区别在于它不会主动的去执行ReacteEffect所关联的回调方法,而是用一个标记来表示当前的值是否有改变,如果有改变,则重新调用回调方法获取,如果没改动,则直接获取上次计算的值。
二、计算属性核心源码
export type ComputedGetter<T> = (...args: any[]) => T export type ComputedSetter<T> = (v: T) => void export interface WritableComputedOptions<T> { get: ComputedGetter<T> set: ComputedSetter<T> } class ComputedRefImpl<T> { public dep?: Dep = undefined private _value!: T private _dirty = true public readonly effect: ReactiveEffect<T> public readonly __v_isRef = true public readonly [ReactiveFlags.IS_READONLY]: boolean constructor( getter: ComputedGetter<T>, private readonly _setter: ComputedSetter<T>, isReadonly: boolean ) { //内部存储一个ReactiveEffect对象 this.effect = new ReactiveEffect(getter, () => { if (!this._dirty) { //标记下次读取将重新计算值 this._dirty = true //触发依赖更新 triggerRefValue(this) } }) this[ReactiveFlags.IS_READONLY] = isReadonly } 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._dirty = false self._value = self.effect.run()! } //直接获取计算好的值 return self._value } set value(newValue: T) { this._setter(newValue) } } 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方法生成的,这个方法其实就是接收一个get方法和一个set方法,并生成一个ComputedRefImpl类型的对象。ComputedRefImpl类的实现很简单,生成一个ReactiveEffect类型对象,并实现一个value属性的读写方法。
在读的时候收集依赖,并判断是否重新计算。特殊的地方在于这个ReactiveEffect类型的对象接收了第二个参数。
我们细看一下触发依赖更新的代码,如下:
export function triggerEffects( ? dep: Dep | ReactiveEffect[], ? debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { ? // spread into array for stabilization ? for (const effect of isArray(dep) ? dep : [...dep]) { ? ? if (effect !== activeEffect || effect.allowRecurse) { ? ? ? if (__DEV__ && effect.onTrigger) { ? ? ? ? effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) ? ? ? } ? ? ? if (effect.scheduler) { ? ? ? ? effect.scheduler() ? ? ? } else { ? ? ? ? effect.run() ? ? ? } ? ? } ? } }
里面逻辑很简单,遍历依赖里面的ReactiveEffect类型对象,如果存在scheduler方法,就调用这个方法。我们在看ReactiveEffect类的定义,代码如下:
export class ReactiveEffect<T = any> { ? active = true ? deps: Dep[] = [] ? // can be attached after creation ? computed?: boolean ? allowRecurse?: boolean ? onStop?: () => void ? // dev only ? onTrack?: (event: DebuggerEvent) => void ? // dev only ? onTrigger?: (event: DebuggerEvent) => void ? constructor( ? ? public fn: () => T, ? ? public scheduler: EffectScheduler | null = null, ? ? scope?: EffectScope | null ? ) { ? ? recordEffectScope(this, scope) ? } }
此时就可以发现,ComputedRefImpl类里面的effect对象接收的第二个参数就是scheduler方法,因此当有依赖的数据变更时,会执行这个方法,这个方法很好理解。
就是改变标记,意味着下一次读取这个计算属性,你需要重新计算了,不能用之前的缓存值了。
接着触发依赖更新,不用疑惑,因为这个计算属性本身也是响应式的,自身改变需要通知相应的依赖更新。至于这个判断,是百分之百为true的,因为触发依赖需要先添加依赖,而在读取value值添加依赖会将标志置为false。
三、异步计算属性核心源码
const tick = Promise.resolve() const queue: any[] = [] let queued = false const scheduler = (fn: any) => { ? queue.push(fn) ? if (!queued) { ? ? queued = true ? ? tick.then(flush) ? } } const flush = () => { ? for (let i = 0; i < queue.length; i++) { ? ? queue[i]() ? } ? queue.length = 0 ? queued = false } class DeferredComputedRefImpl<T> { ? public dep?: Dep = undefined ? private _value!: T ? private _dirty = true ? public readonly effect: ReactiveEffect<T> ? public readonly __v_isRef = true ? public readonly [ReactiveFlags.IS_READONLY] = true ? constructor(getter: ComputedGetter<T>) { ? ? let compareTarget: any ? ? let hasCompareTarget = false ? ? let scheduled = false ? ? this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => { ? ? ? if (this.dep) { ? ? ? ? if (computedTrigger) { ? ? ? ? ? compareTarget = this._value ? ? ? ? ? hasCompareTarget = true ? ? ? ? } else if (!scheduled) { ? ? ? ? ? const valueToCompare = hasCompareTarget ? compareTarget : this._value ? ? ? ? ? scheduled = true ? ? ? ? ? hasCompareTarget = false ? ? ? ? ? //加入执行队列 ? ? ? ? ? scheduler(() => { ? ? ? ? ? ? if (this.effect.active && this._get() !== valueToCompare) { ? ? ? ? ? ? ? triggerRefValue(this) ? ? ? ? ? ? } ? ? ? ? ? ? scheduled = false ? ? ? ? ? }) ? ? ? ? } ? ? ? ? // chained upstream computeds are notified synchronously to ensure ? ? ? ? // value invalidation in case of sync access; normal effects are ? ? ? ? // deferred to be triggered in scheduler. ? ? ? ? for (const e of this.dep) { ? ? ? ? ? if (e.computed) { ? ? ? ? ? ? e.scheduler!(true /* computedTrigger */) ? ? ? ? ? } ? ? ? ? } ? ? ? } ? ? ? //保证异步方法获取值时是重新计算的。 ? ? ? this._dirty = true ? ? }) ? ? this.effect.computed = true ? } ? private _get() { ? ? if (this._dirty) { ? ? ? this._dirty = false ? ? ? return (this._value = this.effect.run()!) ? ? } ? ? return this._value ? } ? get value() { ? ? trackRefValue(this) ? ? // the computed ref may get wrapped by other proxies e.g. readonly() #3376 ? ? return toRaw(this)._get() ? } } export function deferredComputed<T>(getter: () => T): ComputedRef<T> { ? return new DeferredComputedRefImpl(getter) as any }
异步计算属性和计算属性结构几乎一致,最为主要的区别在于ReactiveEffect类型对象的第二个参数上的不同。
这个方法当依赖的某个数据变更时调用,我们先不管第一个if判断,直接看else里面的内容,简单来说就是将一个方法放入异步执行队列里面,然后异步执行。因为当依赖数据变更时,_dirty属性被置为了true,所以这个二异步执行的方法会去计算最新的值并触发依赖更新。
我们现在看if里面的内容,这个分支是通过下面代码进入的。
for (const e of this.dep) { ? if (e.computed) { ? ? e.scheduler!(true /* computedTrigger */) ? } }
这儿的设计原理其实是因为当同步的获取异步计算属性时,会取到最新的值,当执行异步方法时,由于已经获取过一次数据,为了保证this._get() !== valueToCompare判断值是true,valueToCompare必须等于重新计算之前的值。
可以通过以下示例解释:
? ? const src = ref(0) ? ? const c1 = deferredComputed(() => { ? ? ? return src.value % 2 ? ? }) ? ? const c2 = deferredComputed(() => { ? ? ? return c1.value + 1 ? ? }) ? ? effect(() => { ? ? ? c2.value ? ? }) ? ? src.value = 1 ? ? //同步打印c2.value,输出2 ? ? console.log(c2.value);
上述流程,当赋值src.value = 1时,c1执行回调,由于c2依赖c1的值,所以c2也会执行回调,这儿的回调都是指scheduler方法,_dirty属性会被置为true,所以在同步打印c2.value的值时,会去重新计算c2,此时c1的_dirty属性也被置为了true,所以c1的值也会重新计算,即同步打印的c2会取到最新的值。
但需要注意的时,此时异步队列里面的方法还未执行。当同步代码执行完后,开始执行异步队列里面的方法,但执行到如下代码时:
if (this.effect.active && this._get() !== valueToCompare) { ? ? triggerRefValue(this) }
由于同步打印过c2.value的值,此时_get()方法会从缓存里面取值,如果valueToCompare不等于计算前的值,而直接等于this._value,则判断为false,不会触发下面的依赖更新方法。
异步计算属性的核心思想,其实就只是把依赖更新的逻辑放入了异步队列,通过异步的形式执行,其主要逻辑和计算属性几乎一致,只在细节上略有不同。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持。
查看更多关于Vue3计算属性和异步计算属性方式的详细内容...