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

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

ref

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

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

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

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

而 ref 到底是如何實現的呢?github

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

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 布爾類型的屬性。post

函數體內直接返回了 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。this

函數的邏輯爲先使用 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,再次謝謝各位可愛的看官老爺。

相關文章
相關標籤/搜索