Vue3計算屬性和異步計算屬性方式
一、簡要介紹
不論是計算屬性,還是異步計算屬性,都是依托于Vue3整體的響應式原理實現的。其核心依舊是ReacetEffect類。如果對響應式原理不清楚,建議先看響應式原理章節(jié)。
計算屬性和常規(guī)的動態(tài)響應區(qū)別在于它不會主動的去執(zhí)行ReacteEffect所關聯(lián)的回調方法,而是用一個標記來表示當前的值是否有改變,如果有改變,則重新調用回調方法獲取,如果沒改動,則直接獲取上次計算的值。
二、計算屬性核心源碼
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 //觸發(fā)依賴更新 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類型的對象接收了第二個參數。
我們細看一下觸發(fā)依賴更新的代碼,如下:
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) ? } }
此時就可以發(fā)現,ComputedRefImpl類里面的effect對象接收的第二個參數就是scheduler方法,因此當有依賴的數據變更時,會執(zhí)行這個方法,這個方法很好理解。
就是改變標記,意味著下一次讀取這個計算屬性,你需要重新計算了,不能用之前的緩存值了。
接著觸發(fā)依賴更新,不用疑惑,因為這個計算屬性本身也是響應式的,自身改變需要通知相應的依賴更新。至于這個判斷,是百分之百為true的,因為觸發(fā)依賴需要先添加依賴,而在讀取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 ? ? ? ? ? //加入執(zhí)行隊列 ? ? ? ? ? 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 }
異步計算屬性和計算屬性結構幾乎一致,最為主要的區(qū)別在于ReactiveEffect類型對象的第二個參數上的不同。
這個方法當依賴的某個數據變更時調用,我們先不管第一個if判斷,直接看else里面的內容,簡單來說就是將一個方法放入異步執(zhí)行隊列里面,然后異步執(zhí)行。因為當依賴數據變更時,_dirty屬性被置為了true,所以這個二異步執(zhí)行的方法會去計算最新的值并觸發(fā)依賴更新。
我們現在看if里面的內容,這個分支是通過下面代碼進入的。
for (const e of this.dep) { ? if (e.computed) { ? ? e.scheduler!(true /* computedTrigger */) ? } }
這兒的設計原理其實是因為當同步的獲取異步計算屬性時,會取到最新的值,當執(zhí)行異步方法時,由于已經獲取過一次數據,為了保證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執(zhí)行回調,由于c2依賴c1的值,所以c2也會執(zhí)行回調,這兒的回調都是指scheduler方法,_dirty屬性會被置為true,所以在同步打印c2.value的值時,會去重新計算c2,此時c1的_dirty屬性也被置為了true,所以c1的值也會重新計算,即同步打印的c2會取到最新的值。
但需要注意的時,此時異步隊列里面的方法還未執(zhí)行。當同步代碼執(zhí)行完后,開始執(zhí)行異步隊列里面的方法,但執(zhí)行到如下代碼時:
if (this.effect.active && this._get() !== valueToCompare) { ? ? triggerRefValue(this) }
由于同步打印過c2.value的值,此時_get()方法會從緩存里面取值,如果valueToCompare不等于計算前的值,而直接等于this._value,則判斷為false,不會觸發(fā)下面的依賴更新方法。
異步計算屬性的核心思想,其實就只是把依賴更新的邏輯放入了異步隊列,通過異步的形式執(zhí)行,其主要邏輯和計算屬性幾乎一致,只在細節(jié)上略有不同。
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
vue-router重寫push方法,解決相同路徑跳轉報錯問題
這篇文章主要介紹了vue-router重寫push方法,解決相同路徑跳轉報錯問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08Vue中使用 Echarts5.0 遇到的一些問題(vue-cli 下開發(fā))
這篇文章主要介紹了Vue中使用 Echarts5.0 遇到的一些問題(vue-cli 下開發(fā)),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10