在 Vue3 新推出的響應(yīng)式 API 中,Ref 系列毫無(wú)疑問(wèn)是使用頻率最高的 api 之一,而 computed 計(jì)算屬性是一個(gè)在上一個(gè)版本中就非常熟悉的選項(xiàng)了,但是在 Vue3 中也提供了獨(dú)立的 api 方便我們直接創(chuàng)建計(jì)算值。而今天這篇文章,筆者就會(huì)給大家講解 ref 與 computed 的實(shí)現(xiàn)原理,讓我們一起開(kāi)始本章的學(xué)習(xí)吧。
ref
當(dāng)我們有一個(gè)獨(dú)立的原始值,例如一個(gè)字符串,我們想讓它變成響應(yīng)式的時(shí)候可以通過(guò)創(chuàng)建一個(gè)對(duì)象,將這個(gè)字符串以鍵值對(duì)的形式放入對(duì)象中,然后傳遞給 reactive。而 Vue 為我們提供了一個(gè)更容易的方式,通過(guò) ref 來(lái)完成。
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
ref 會(huì)返回一個(gè)可變的響應(yīng)式對(duì)象,該對(duì)象作為一個(gè)響應(yīng)式的引用維護(hù)著它內(nèi)部的值,這就是 ref 名稱的來(lái)源。該對(duì)象只包含一個(gè)名為 value 的 property。
而 ref 究竟是如何實(shí)現(xiàn)的呢?
ref 的源碼位置在 @vue/reactivity 的庫(kù)內(nèi),路徑是 packages/reactivity/src/ref.ts ,接下來(lái)我們就一起來(lái)看 ref 的實(shí)現(xiàn)。
export function ref<T extends object>(value: T): ToRef<T>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value)
}
從 ref api 的函數(shù)簽名中,可以看到 ref 函數(shù)接收一個(gè)任意類型的值作為它的 value 參數(shù),并返回一個(gè) Ref 類型的值。
export interface Ref<T = any> {
value: T
[RefSymbol]: true
_shallow?: boolean
}
從返回值 Ref 的類型定義中看出,ref 的返回值中有一個(gè) value 屬性,以及有一個(gè)私有的 symbol key,還有一個(gè)標(biāo)識(shí)是否為 shallowRef 的_shallow 布爾類型的屬性。
函數(shù)體內(nèi)直接返回了 createRef 函數(shù)的返回值。
createRef
function createRef(rawValue: unknown, shallow = false) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
createRef 的實(shí)現(xiàn)也很簡(jiǎn)單,入?yún)?rawValue 與 shallow,rawValue 記錄的創(chuàng)建 ref 的原始值,而 shallow 則是表明是否為 shallowRef 的淺層響應(yīng)式 api。
函數(shù)的邏輯為先使用 isRef 判斷是否為 rawValue,如果是的話則直接返回這個(gè) ref 對(duì)象。
否則返回一個(gè)新創(chuàng)建的 RefImpl 類的實(shí)例對(duì)象。
RefImpl 類
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow: boolean) {
// 如果是 shallow 淺層響應(yīng),則直接將 _value 置為 _rawValue,否則通過(guò) convert 處理 _rawValue
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
// 讀取 value 前,先通過(guò) track 收集 value 依賴
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
// 如果需要更新
if (hasChanged(toRaw(newVal), this._rawValue)) {
// 更新 _rawValue 與 _value
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
// 通過(guò) trigger 派發(fā) value 更新
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
在 RefImpl 類中,有一個(gè)私有變量 _value 用來(lái)存儲(chǔ) ref 的最新的值;公共的只讀變量 __v_isRef 是用來(lái)標(biāo)識(shí)該對(duì)象是一個(gè) ref 響應(yīng)式對(duì)象的標(biāo)記與在講述 reactive api 時(shí)的 ReactiveFlag 相同。
而在 RefImpl 的構(gòu)造函數(shù)中,接受一個(gè)私有的 _rawValue 變量,存放 ref 的舊值;公共的 _shallow 變量是區(qū)分是否為淺層響應(yīng)的。在構(gòu)造函數(shù)內(nèi)部,先判斷 _shallow 是否為 true,如果是 shallowRef ,則直接將原始值賦值給 _value,否則會(huì)通過(guò) convert 進(jìn)行轉(zhuǎn)換再賦值。
在 conver 函數(shù)的內(nèi)部,其實(shí)就是判斷傳入的參數(shù)是否是一個(gè)對(duì)象,如果是一個(gè)對(duì)象則通過(guò) reactive api 創(chuàng)建一個(gè)代理對(duì)象并返回,否則直接返回原參數(shù)。
當(dāng)我們通過(guò) ref.value 的形式讀取該 ref 的值時(shí),就會(huì)觸發(fā) value 的 getter 方法,在 getter 中會(huì)先通過(guò) track 收集該 ref 對(duì)象的 value 的依賴,收集完畢后返回該 ref 的值。
當(dāng)我們對(duì) ref.value 進(jìn)行修改時(shí),又會(huì)觸發(fā) value 的 setter 方法,會(huì)將新舊 value 進(jìn)行比較,如果值不同需要更新,則先更新新舊 value,之后通過(guò) trigger 派發(fā)該 ref 對(duì)象的 value 屬性的更新,讓依賴該 ref 的副作用函數(shù)執(zhí)行更新。
如果有朋友對(duì)于 track 收集依賴,trigger 派發(fā)更新比較迷糊的話,建議先閱讀我的上一篇文章,在上一篇文章中筆者仔細(xì)講解了這個(gè)過(guò)程,至此 ref 的實(shí)現(xiàn)筆者就給大家解釋清楚了。
computed
在文檔中關(guān)于 computed api 是這樣介紹的:接受一個(gè) getter 函數(shù),并以 getter 函數(shù)的返回值返回一個(gè)不可變的響應(yīng)式 ref 對(duì)象。或者它也可以使用具有 get 和 set 函數(shù)的對(duì)象來(lái)創(chuàng)建一個(gè)可寫(xiě)的 ref 對(duì)象。
computed 函數(shù)
根據(jù)這個(gè) api 的描述,顯而易見(jiàn)的能夠知道 computed 接受一個(gè)函數(shù)或是對(duì)象類型的參數(shù),所以我們先從它的函數(shù)簽名看起。
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
)
在 computed 函數(shù)的重載中,代碼第一行接收 getter 類型的參數(shù),并返回 ComputedRef 類型的函數(shù)簽名是文檔中描述的第一種情況,接受 getter 函數(shù),并以 getter 函數(shù)的返回值返回一個(gè)不可變的響應(yīng)式 ref 對(duì)象。
而在第二行代碼中,computed 函數(shù)接受一個(gè) options 對(duì)象,并返回一個(gè)可寫(xiě)的 ComputedRef 類型,是文檔的第二種情況,創(chuàng)建一個(gè)可寫(xiě)的 ref 對(duì)象。
第三行代碼,則是這個(gè)函數(shù)重載的最寬泛情況,參數(shù)名已經(jīng)提現(xiàn)了這一點(diǎn):getterOrOptions。
一起看一下 computed api 中相關(guān)的類型定義:
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
readonly value: T
}
export interface WritableComputedRef<T> extends Ref<T> {
readonly effect: ReactiveEffect<T>
}
export type ComputedGetter<T> = (ctx?: any) => T
export type ComputedSetter<T> = (v: T) => void
export interface WritableComputedOptions<T> {
get: ComputedGetter<T>
set: ComputedSetter<T>
}
從類型定義中得知:WritableComputedRef 以及 ComputedRef 都是擴(kuò)展自 Ref 類型的,這也就理解了文檔中為什么說(shuō) computed 返回的是一個(gè) ref 類型的響應(yīng)式對(duì)象。
接下來(lái)看一下 computed api 的函數(shù)體內(nèi)的完整邏輯:
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
// 如果 參數(shù) getterOrOptions 是一個(gè)函數(shù)
if (isFunction(getterOrOptions)) {
// 那么這個(gè)函數(shù)必然就是 getter,將函數(shù)賦值給 getter
getter = getterOrOptions
// 這種場(chǎng)景下如果在 DEV 環(huán)境下訪問(wèn) setter 則報(bào)出警告
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
// 這個(gè)判斷里,說(shuō)明參數(shù)是一個(gè) options,則取 get、set 賦值即可
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return new ComputedRefImpl(
getter,
setter,
isFunction(getterOrOptions) || !getterOrOptions.set
) as any
}
在 computed api 中,首先會(huì)判斷傳入的參數(shù)是一個(gè) getter 函數(shù)還是 options 對(duì)象,如果是函數(shù)的話則這個(gè)函數(shù)只能是 getter 函數(shù)無(wú)疑,此時(shí)將 getter 賦值,并且在 DEV 環(huán)境中訪問(wèn) setter 不會(huì)成功,同時(shí)會(huì)報(bào)出警告。如果傳入是不是函數(shù),computed 就會(huì)將它作為一個(gè)帶有 get、set 屬性的對(duì)象處理,將對(duì)象中的 get、set 賦值給對(duì)應(yīng)的 getter、setter。最后在處理完成后,會(huì)返回一個(gè) ComputedRefImpl 類的實(shí)例對(duì)象,computed api 就處理完成。
ComputedRefImpl 類
這個(gè)類與我們之前介紹的 RefImpl Class 類似,但構(gòu)造函數(shù)中的邏輯有點(diǎn)區(qū)別。
先看類中的成員變量:
class ComputedRefImpl<T> {
private _value!: T
private _dirty = true
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true;
public readonly [ReactiveFlags.IS_READONLY]: boolean
}
跟 RefImpl 類相比,增加了 _dirty 私有成員變量,一個(gè) effect 的只讀副作用函數(shù)變量,以及增加了一個(gè) __v_isReadonly 標(biāo)記。
接著看一下構(gòu)造函數(shù)中的邏輯:
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean
) {
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value')
}
}
})
this[ReactiveFlags.IS_READONLY] = isReadonly
}
構(gòu)造函數(shù)中,會(huì)為 getter 創(chuàng)建一個(gè)副作用函數(shù),并且在副作用選項(xiàng)中設(shè)置為延遲執(zhí)行,并且增加了調(diào)度器。在調(diào)度器中會(huì)判斷 this._dirty 標(biāo)記是否為 false,如果是的話,將 this._dirty 置為 true,并且利用 trigger 派發(fā)更新。如果對(duì)這個(gè)副作用的執(zhí)行時(shí)機(jī),以及副作用中調(diào)度器是什么時(shí)候執(zhí)行這些問(wèn)題犯迷糊的同學(xué),還是建議閱讀上一篇文章,先把 effect 副作用搞明白,再去理解響應(yīng)式的其他 api 必然是事半功倍的。
get value() {
// 這個(gè) computed ref 有可能是被其他代理對(duì)象包裹的
const self = toRaw(this)
if (self._dirty) {
// getter 時(shí)執(zhí)行副作用函數(shù),派發(fā)更新,這樣能更新依賴的值
self._value = this.effect()
self._dirty = false
}
// 調(diào)用 track 收集依賴
track(self, TrackOpTypes.GET, 'value')
// 返回最新的值
return self._value
}
set value(newValue: T) {
// 執(zhí)行 setter 函數(shù)
this._setter(newValue)
}
在 computed 中,通過(guò) getter 函數(shù)獲取值時(shí),會(huì)先執(zhí)行副作用函數(shù),并將副作用函數(shù)的返回值賦值給 _value,并將 _dirty 的值賦值給 false,這就可以保證如果 computed 中的依賴沒(méi)有發(fā)生變化,則副作用函數(shù)不會(huì)再次執(zhí)行,那么在 getter 時(shí)獲取到的 _dirty 始終是 false,也不需要再次執(zhí)行副作用函數(shù),節(jié)約開(kāi)銷。之后通過(guò) track 收集依賴,并返回 _value 的值。
而在 setter 中,只是執(zhí)行我們傳入的 setter 邏輯,至此 computed api 的實(shí)現(xiàn)也已經(jīng)講解完畢了。
總結(jié)
在本文中,以上文副作用函數(shù)和依賴收集派發(fā)更新的知識(shí)點(diǎn)為基礎(chǔ),筆者為大家講解了 ref 和 computed 兩個(gè)在 Vue3 響應(yīng)式中最常用的 api 的實(shí)現(xiàn),這兩個(gè) api 都是在創(chuàng)建時(shí)返回了一個(gè)類實(shí)例,在實(shí)例中的構(gòu)造函數(shù)以及對(duì) value 屬性設(shè)置的 get 和 set 完成響應(yīng)式追蹤。
當(dāng)我們?cè)趯W(xué)會(huì)使用這些的同時(shí),并能知其所以然一定能夠幫我們?cè)谑褂眠@些 api 時(shí)發(fā)揮出它最大的作用,同時(shí)也能夠讓你在寫(xiě)出了一些不符合你預(yù)期代碼的時(shí)候,快速的定位問(wèn)題,能搞定究竟是自己寫(xiě)的不對(duì),還是本身 api 并不支持某種調(diào)用方式。
最后,如果這篇文章能夠幫助到你了解 Vue3 中的響應(yīng)式 api ref 和 computed 的實(shí)現(xiàn)原理,希望能給本文點(diǎn)一個(gè)喜歡??。如果想繼續(xù)追蹤后續(xù)文章,也可以關(guān)注我的賬號(hào)或 follow 我的 github,再次謝謝各位可愛(ài)的看官老爺。