Vue3 源碼解析(八):ref 與 computed 原理揭秘

在 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)的看官老爺。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容