Vue3計(jì)算屬性和異步計(jì)算屬性方式
一、簡要介紹
不論是計(jì)算屬性,還是異步計(jì)算屬性,都是依托于Vue3整體的響應(yīng)式原理實(shí)現(xiàn)的。其核心依舊是ReacetEffect類。如果對(duì)響應(yīng)式原理不清楚,建議先看響應(yīng)式原理章節(jié)。
計(jì)算屬性和常規(guī)的動(dòng)態(tài)響應(yīng)區(qū)別在于它不會(huì)主動(dòng)的去執(zhí)行ReacteEffect所關(guān)聯(lián)的回調(diào)方法,而是用一個(gè)標(biāo)記來表示當(dāng)前的值是否有改變,如果有改變,則重新調(diào)用回調(diào)方法獲取,如果沒改動(dòng),則直接獲取上次計(jì)算的值。
二、計(jì)算屬性核心源碼
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 ) { //內(nèi)部存儲(chǔ)一個(gè)ReactiveEffect對(duì)象 this.effect = new ReactiveEffect(getter, () => { if (!this._dirty) { //標(biāo)記下次讀取將重新計(jì)算值 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) //是否重新計(jì)算標(biāo)記 if (self._dirty) { //重新計(jì)算 self._dirty = false self._value = self.effect.run()! } //直接獲取計(jì)算好的值 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 } //只需要關(guān)注這兒 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 }
一個(gè)計(jì)算屬性對(duì)象的生成都是通過computed方法生成的,這個(gè)方法其實(shí)就是接收一個(gè)get方法和一個(gè)set方法,并生成一個(gè)ComputedRefImpl類型的對(duì)象。ComputedRefImpl類的實(shí)現(xiàn)很簡單,生成一個(gè)ReactiveEffect類型對(duì)象,并實(shí)現(xiàn)一個(gè)value屬性的讀寫方法。
在讀的時(shí)候收集依賴,并判斷是否重新計(jì)算。特殊的地方在于這個(gè)ReactiveEffect類型的對(duì)象接收了第二個(gè)參數(shù)。
我們細(xì)看一下觸發(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() ? ? ? } ? ? } ? } }
里面邏輯很簡單,遍歷依賴?yán)锩娴腞eactiveEffect類型對(duì)象,如果存在scheduler方法,就調(diào)用這個(gè)方法。我們在看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) ? } }
此時(shí)就可以發(fā)現(xiàn),ComputedRefImpl類里面的effect對(duì)象接收的第二個(gè)參數(shù)就是scheduler方法,因此當(dāng)有依賴的數(shù)據(jù)變更時(shí),會(huì)執(zhí)行這個(gè)方法,這個(gè)方法很好理解。
就是改變標(biāo)記,意味著下一次讀取這個(gè)計(jì)算屬性,你需要重新計(jì)算了,不能用之前的緩存值了。
接著觸發(fā)依賴更新,不用疑惑,因?yàn)檫@個(gè)計(jì)算屬性本身也是響應(yīng)式的,自身改變需要通知相應(yīng)的依賴更新。至于這個(gè)判斷,是百分之百為true的,因?yàn)橛|發(fā)依賴需要先添加依賴,而在讀取value值添加依賴會(huì)將標(biāo)志置為false。
三、異步計(jì)算屬性核心源碼
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í)行隊(duì)列 ? ? ? ? ? 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 */) ? ? ? ? ? } ? ? ? ? } ? ? ? } ? ? ? //保證異步方法獲取值時(shí)是重新計(jì)算的。 ? ? ? 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 }
異步計(jì)算屬性和計(jì)算屬性結(jié)構(gòu)幾乎一致,最為主要的區(qū)別在于ReactiveEffect類型對(duì)象的第二個(gè)參數(shù)上的不同。
這個(gè)方法當(dāng)依賴的某個(gè)數(shù)據(jù)變更時(shí)調(diào)用,我們先不管第一個(gè)if判斷,直接看else里面的內(nèi)容,簡單來說就是將一個(gè)方法放入異步執(zhí)行隊(duì)列里面,然后異步執(zhí)行。因?yàn)楫?dāng)依賴數(shù)據(jù)變更時(shí),_dirty屬性被置為了true,所以這個(gè)二異步執(zhí)行的方法會(huì)去計(jì)算最新的值并觸發(fā)依賴更新。
我們現(xiàn)在看if里面的內(nèi)容,這個(gè)分支是通過下面代碼進(jìn)入的。
for (const e of this.dep) { ? if (e.computed) { ? ? e.scheduler!(true /* computedTrigger */) ? } }
這兒的設(shè)計(jì)原理其實(shí)是因?yàn)楫?dāng)同步的獲取異步計(jì)算屬性時(shí),會(huì)取到最新的值,當(dāng)執(zhí)行異步方法時(shí),由于已經(jīng)獲取過一次數(shù)據(jù),為了保證this._get() !== valueToCompare判斷值是true,valueToCompare必須等于重新計(jì)算之前的值。
可以通過以下示例解釋:
? ? 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);
上述流程,當(dāng)賦值src.value = 1時(shí),c1執(zhí)行回調(diào),由于c2依賴c1的值,所以c2也會(huì)執(zhí)行回調(diào),這兒的回調(diào)都是指scheduler方法,_dirty屬性會(huì)被置為true,所以在同步打印c2.value的值時(shí),會(huì)去重新計(jì)算c2,此時(shí)c1的_dirty屬性也被置為了true,所以c1的值也會(huì)重新計(jì)算,即同步打印的c2會(huì)取到最新的值。
但需要注意的時(shí),此時(shí)異步隊(duì)列里面的方法還未執(zhí)行。當(dāng)同步代碼執(zhí)行完后,開始執(zhí)行異步隊(duì)列里面的方法,但執(zhí)行到如下代碼時(shí):
if (this.effect.active && this._get() !== valueToCompare) { ? ? triggerRefValue(this) }
由于同步打印過c2.value的值,此時(shí)_get()方法會(huì)從緩存里面取值,如果valueToCompare不等于計(jì)算前的值,而直接等于this._value,則判斷為false,不會(huì)觸發(fā)下面的依賴更新方法。
異步計(jì)算屬性的核心思想,其實(shí)就只是把依賴更新的邏輯放入了異步隊(duì)列,通過異步的形式執(zhí)行,其主要邏輯和計(jì)算屬性幾乎一致,只在細(xì)節(jié)上略有不同。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
ssm+vue前后端分離框架整合實(shí)現(xiàn)(附源碼)
這篇文章主要介紹了ssm+vue前后端分離框架整合實(shí)現(xiàn)(附源碼),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07vue-router重寫push方法,解決相同路徑跳轉(zhuǎn)報(bào)錯(cuò)問題
這篇文章主要介紹了vue-router重寫push方法,解決相同路徑跳轉(zhuǎn)報(bào)錯(cuò)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08關(guān)于vue.js組件數(shù)據(jù)流的問題
本篇文章主要介紹了關(guān)于vue.js組件數(shù)據(jù)流的問題,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07Vue中使用 Echarts5.0 遇到的一些問題(vue-cli 下開發(fā))
這篇文章主要介紹了Vue中使用 Echarts5.0 遇到的一些問題(vue-cli 下開發(fā)),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10vue調(diào)用本地緩存方式(監(jiān)視數(shù)據(jù)變更)
這篇文章主要介紹了vue調(diào)用本地緩存方式(監(jiān)視數(shù)據(jù)變更),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04