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

在 Vue3 新推出的響應式 API 中,Ref 系列毫無疑問是使用頻率最高的 api 之一,而 computed 計算屬性是一個在上一個版本中就非常熟悉的選項了,但是在 Vue3 中也提供了獨立的 api 方便我們直接創建計算值。而今天這篇文章,筆者就會給大家講解 ref 與 computed 的實現原理,讓我們一起開始本章的學習吧。

ref

當我們有一個獨立的原始值,例如一個字符串,我們想讓它變成響應式的時候可以通過創建一個對象,將這個字符串以鍵值對的形式放入對象中,然後傳遞給 reactive。而 Vue 爲我們提供了一個更容易的方式,通過 ref 來完成。

import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

ref 會返回一個可變的響應式對象,該對象作爲一個響應式的引用維護着它內部的值,這就是 ref 名稱的來源。該對象只包含一個名爲 value 的 property。

而 ref 究竟是如何實現的呢?

ref 的源碼位置在 @vue/reactivity 的庫內,路徑是 packages/reactivity/src/ref.ts ,接下來我們就一起來看 ref 的實現。

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 的函數簽名中,可以看到 ref 函數接收一個任意類型的值作爲它的 value 參數,並返回一個 Ref 類型的值。

export interface Ref<T = any> {
  value: T
  [RefSymbol]: true
  _shallow?: boolean
}

從返回值 Ref 的類型定義中看出,ref 的返回值中有一個 value 屬性,以及有一個私有的 symbol key,還有一個標識是否爲 shallowRef 的_shallow 布爾類型的屬性。

函數體內直接返回了 createRef 函數的返回值。

createRef

function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

createRef 的實現也很簡單,入參爲 rawValue 與 shallow,rawValue 記錄的創建 ref 的原始值,而 shallow 則是表明是否爲 shallowRef 的淺層響應式 api。

函數的邏輯爲先使用 isRef 判斷是否爲 rawValue,如果是的話則直接返回這個 ref 對象。

否則返回一個新創建的 RefImpl 類的實例對象。

RefImpl 類

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, public readonly _shallow: boolean) {
    // 如果是 shallow 淺層響應,則直接將 _value 置爲 _rawValue,否則通過 convert 處理 _rawValue
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    // 讀取 value 前,先通過 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)
      // 通過 trigger 派發 value 更新
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

在 RefImpl 類中,有一個私有變量 _value 用來存儲 ref 的最新的值;公共的只讀變量 __v_isRef 是用來標識該對象是一個 ref 響應式對象的標記與在講述 reactive api 時的 ReactiveFlag 相同。

而在 RefImpl 的構造函數中,接受一個私有的 _rawValue 變量,存放 ref 的舊值;公共的 _shallow 變量是區分是否爲淺層響應的。在構造函數內部,先判斷 _shallow 是否爲 true,如果是 shallowRef ,則直接將原始值賦值給 _value,否則會通過 convert 進行轉換再賦值。

在 conver 函數的內部,其實就是判斷傳入的參數是否是一個對象,如果是一個對象則通過 reactive api 創建一個代理對象並返回,否則直接返回原參數。

當我們通過 ref.value 的形式讀取該 ref 的值時,就會觸發 value 的 getter 方法,在 getter 中會先通過 track 收集該 ref 對象的 value 的依賴,收集完畢後返回該 ref 的值。

當我們對 ref.value 進行修改時,又會觸發 value 的 setter 方法,會將新舊 value 進行比較,如果值不同需要更新,則先更新新舊 value,之後通過 trigger 派發該 ref 對象的 value 屬性的更新,讓依賴該 ref 的副作用函數執行更新。

如果有朋友對於 track 收集依賴,trigger 派發更新比較迷糊的話,建議先閱讀我的上一篇文章,在上一篇文章中筆者仔細講解了這個過程,至此 ref 的實現筆者就給大家解釋清楚了。

computed

在文檔中關於 computed api 是這樣介紹的:接受一個 getter 函數,並以 getter 函數的返回值返回一個不可變的響應式 ref 對象。或者它也可以使用具有 get 和 set 函數的對象來創建一個可寫的 ref 對象。

computed 函數

根據這個 api 的描述,顯而易見的能夠知道 computed 接受一個函數或是對象類型的參數,所以我們先從它的函數簽名看起。

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 函數的重載中,代碼第一行接收 getter 類型的參數,並返回 ComputedRef 類型的函數簽名是文檔中描述的第一種情況,接受 getter 函數,並以 getter 函數的返回值返回一個不可變的響應式 ref 對象。

而在第二行代碼中,computed 函數接受一個 options 對象,並返回一個可寫的 ComputedRef 類型,是文檔的第二種情況,創建一個可寫的 ref 對象。

第三行代碼,則是這個函數重載的最寬泛情況,參數名已經提現了這一點:getterOrOptions。

一起看一下 computed api 中相關的類型定義:

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 都是擴展自 Ref 類型的,這也就理解了文檔中爲什麼說 computed 返回的是一個 ref 類型的響應式對象。

接下來看一下 computed api 的函數體內的完整邏輯:

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  // 如果 參數 getterOrOptions 是一個函數
  if (isFunction(getterOrOptions)) {
    // 那麼這個函數必然就是 getter,將函數賦值給 getter
    getter = getterOrOptions
    // 這種場景下如果在 DEV 環境下訪問 setter 則報出警告
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    // 這個判斷裏,說明參數是一個 options,則取 get、set 賦值即可
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  
  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any
}

在 computed api 中,首先會判斷傳入的參數是一個 getter 函數還是 options 對象,如果是函數的話則這個函數只能是 getter 函數無疑,此時將 getter 賦值,並且在 DEV 環境中訪問 setter 不會成功,同時會報出警告。如果傳入是不是函數,computed 就會將它作爲一個帶有 get、set 屬性的對象處理,將對象中的 get、set 賦值給對應的 getter、setter。最後在處理完成後,會返回一個 ComputedRefImpl 類的實例對象,computed api 就處理完成。

ComputedRefImpl 類

這個類與我們之前介紹的 RefImpl Class 類似,但構造函數中的邏輯有點區別。

先看類中的成員變量:

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 私有成員變量,一個 effect 的只讀副作用函數變量,以及增加了一個 __v_isReadonly 標記。

接着看一下構造函數中的邏輯:

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
}

構造函數中,會爲 getter 創建一個副作用函數,並且在副作用選項中設置爲延遲執行,並且增加了調度器。在調度器中會判斷 this._dirty 標記是否爲 false,如果是的話,將 this._dirty 置爲 true,並且利用 trigger 派發更新。如果對這個副作用的執行時機,以及副作用中調度器是什麼時候執行這些問題犯迷糊的同學,還是建議閱讀上一篇文章,先把 effect 副作用搞明白,再去理解響應式的其他 api 必然是事半功倍的。

get value() {
  // 這個 computed ref 有可能是被其他代理對象包裹的
  const self = toRaw(this)
  if (self._dirty) {
    // getter 時執行副作用函數,派發更新,這樣能更新依賴的值
    self._value = this.effect()
    self._dirty = false
  }
  // 調用 track 收集依賴
  track(self, TrackOpTypes.GET, 'value')
  // 返回最新的值
  return self._value
}

set value(newValue: T) {
  // 執行 setter 函數
  this._setter(newValue)
}

在 computed 中,通過 getter 函數獲取值時,會先執行副作用函數,並將副作用函數的返回值賦值給 _value,並將 _dirty 的值賦值給 false,這就可以保證如果 computed 中的依賴沒有發生變化,則副作用函數不會再次執行,那麼在 getter 時獲取到的 _dirty 始終是 false,也不需要再次執行副作用函數,節約開銷。之後通過 track 收集依賴,並返回 _value 的值。

而在 setter 中,只是執行我們傳入的 setter 邏輯,至此 computed api 的實現也已經講解完畢了。

總結

在本文中,以上文副作用函數和依賴收集派發更新的知識點爲基礎,筆者爲大家講解了 ref 和 computed 兩個在 Vue3 響應式中最常用的 api 的實現,這兩個 api 都是在創建時返回了一個類實例,在實例中的構造函數以及對 value 屬性設置的 get 和 set 完成響應式追蹤。

當我們在學會使用這些的同時,並能知其所以然一定能夠幫我們在使用這些 api 時發揮出它最大的作用,同時也能夠讓你在寫出了一些不符合你預期代碼的時候,快速的定位問題,能搞定究竟是自己寫的不對,還是本身 api 並不支持某種調用方式。

最後,如果這篇文章能夠幫助到你瞭解 Vue3 中的響應式 api ref 和 computed 的實現原理,希望能給本文點一個喜歡❤️。如果想繼續追蹤後續文章,也可以關注我的賬號或 follow 我的 github,再次謝謝各位可愛的看官老爺。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章