Vue3響應式系統源碼解析-Ref篇

前言的前言

閱讀本文須要有必定的TypeScript基礎,要求不高,看過一遍TS的文檔便可。html

咱們閱讀源碼的緣由是什麼?無非是1:學習;2:更好的使用這個庫。若是隻是想大體的瞭解下原理,倒沒必要花時間閱讀源碼,幾句話,幾張圖就能搞清楚,網上搜搜應該就有不少。所以,閱讀源碼的過程必定是要對不明白的地方深刻了解,確定是很費時間的。vue

在這過程當中,有些知識點,跟庫自己可能沒什麼關係,但若是不懂,又難繼續理解。對於這些知識點,我會盡可能少的解釋,但會貼上儘可能完善的文檔,方便不瞭解的同窗先閱讀學習。react

鑑於篇幅太長,信息量較大,我會將文章拆開,邊寫邊發,有興趣的同窗能夠連載閱讀,寫完之後再彙總一篇,方便時間充沛的同窗一股腦看。git

前言

在上篇文章中說道,ref是最影響源碼閱讀的文件。但若是不先搞明白它,看其餘的只會更暈。我先幫你們理清ref的邏輯跟概念。github

因爲如今(2019/10/9)vue@3還未正式發版,你們還不熟悉其相關的用法。上篇文章雖然介紹了很多,但其實仍是有很多疑問。在閱讀本篇文章以前,若是有時間,建議先閱讀Vue官方對Composition API的介紹:typescript

  1. Vue Composition API
  2. Ref Vs Reactive

讀完關於Composition API的介紹,會對了解本庫有更多認識,便於更好的理解源碼。api

refreactive是整個源碼中的核心,經過這兩個方法建立了響應式數據。要想徹底吃透reactivity,必須先吃透這兩個。數組

Ref

ref最重要的做用,實際上是提供了一套Ref類型,咱們先來看,它究竟是個怎麼樣的數據類型。(爲了更好的作解釋,我會調整源碼中的接口、類型、函數等聲明順序,並會增長一些註釋方便閱讀)數據結構

// 生成一個惟一key,開發環境下增長描述符 'refSymbol'
export const refSymbol = Symbol(__DEV__ ? 'refSymbol' : undefined)

// 聲明Ref接口
export interface Ref<T = any> {
  // 用此惟一key,來作Ref接口的一個描述符,讓isRef函數作類型判斷
  [refSymbol]: true
  // value值,存放真正的數據的地方。關於UnwrapNestedRefs這個類型,我後續單獨解釋
  value: UnwrapNestedRefs<T>
}

// 判斷是不是Ref數據的方法
// 對於is關鍵詞,若不熟悉,見:http://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates
export function isRef(v: any): v is Ref {
  return v ? v[refSymbol] === true : false
}

// 見下文解釋
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

複製代碼

要想了解UnwrapNestedRefsUnwrapRef,必須先要了解ts中的infer。若是以前不瞭解,請先閱讀相關文檔。看完文檔,再建議去google一些案例看看加深下印象。app

如今咱們假設你瞭解了infer概念,也瞭解了它的平常用法。再來看源碼:

// 不該該繼續遞歸的引用數據類型
type BailTypes =
  | Function
  | Map<any, any>
  | Set<any>
  | WeakMap<any, any>
  | WeakSet<any>

// 遞歸地獲取嵌套數據的類型
// Recursively unwraps nested value bindings.
export type UnwrapRef<T> = {
  // 若是是ref類型,繼續解套
  ref: T extends Ref<infer V> ? UnwrapRef<V> : T
  // 若是是數組,循環解套
  array: T extends Array<infer V> ? Array<UnwrapRef<V>> : T
  // 若是是對象,遍歷解套
  object: { [K in keyof T]: UnwrapRef<T[K]> }
  // 不然,中止解套
  stop: T
}[T extends Ref
  ? 'ref'
  : T extends Array<any>
    ? 'array'
    : T extends BailTypes
      ? 'stop' // bail out on types that shouldn't be unwrapped
      : T extends object ? 'object' : 'stop']

// 聲明類型別名:UnwrapNestedRefs
// 它是這樣的類型:若是該類型已經繼承於Ref,則不須要解套,不然多是嵌套的ref,走遞歸解套
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

複製代碼

若是仍是懵,建議後續再去看看infer的相關介紹。在這咱們直接拋結果:

Ref是這樣的一種數據結構:它有個key爲Symbol的屬性作類型標識,有個屬性value用來存儲數據。這個數據能夠是任意的類型,惟獨不能是被嵌套了Ref類型的類型。 具體來講就是不能是這樣 Array<Ref> 或者這樣 { [key]: Ref }。但很奇怪的是,這樣Ref<Ref> 又是能夠的。具體爲何也不知道,因此我勇敢地提了個PR...

(果真Ref<Ref>是不夠完美的,2019.10.10晚,我這PR被合併了。你們遇到疑問時,也能夠勇敢的提PR,說不定就被合了....)

另外,Map、Set、WeakMap、WeakSet也是不支持解套的。說明Ref數據的value也有多是Map<Ref>這樣的數據類型。

說回Ref,從上篇文章中,咱們已經瞭解到,Ref類型的數據,是一種響應式的數據。而後咱們看其具體實現:

// 從@vue/shared中引入,判斷一個數據是否爲對象
// Record<any, any>表明了任意類型key,任意類型value的類型
// 爲何不是 val is object 呢?能夠看下這個回答:https://stackoverflow.com/questions/52245366/in-typescript-is-there-a-difference-between-types-object-and-recordany-any
export const isObject = (val: any): val is Record<any, any> =>
  val !== null && typeof val === 'object'

// 若是傳遞的值是個對象(包含數組/Map/Set/WeakMap/WeakSet),則使用reactive執行,不然返回原數據
// 從上篇文章知道,這個reactive就是將咱們的數據轉成響應式數據
const convert = (val: any): any => (isObject(val) ? reactive(val) : val)

export function ref<T>(raw: T): Ref<T> {
  // 轉化數據
  raw = convert(raw)
  const v = {
    [refSymbol]: true,
    get value() {
      // track的代碼在effect中,暫時不看,能猜到此處就是監聽函數收集依賴的方法。
      track(v, OperationTypes.GET, '')
      // 返回剛剛被轉化後的數據
      return raw
    },
    set value(newVal) {
      // 將設置的值,轉化爲響應式數據,賦值給raw
      raw = convert(newVal)
      // trigger也暫時不看,能猜到此處就是觸發監聽函數執行的方法
      trigger(v, OperationTypes.SET, '')
    }
  }
  return v as Ref<T>
}
複製代碼

其實最難理解的就在於這個ref函數。咱們看到,這裏也定義了get/set,卻沒有任何Proxy相關的操做。在以前的信息中咱們知道reactive能構建出響應式數據,但要求傳參必須是對象。但ref的入參是對象時,一樣也須要reactive作轉化。那ref這個函數的目的究竟是什麼呢?爲何須要有它?

在文章開頭,我貼了這份官方介紹Ref vs Reactive,這其中其實已經說的很明白。

However, the problem with going reactive-only is that the consumer of a composition function must keep the reference to the returned object at all times in order to retain reactivity. The object cannot be destructured or spread:

對於基本數據類型,函數傳遞或者對象解構時,會丟失原始數據的引用,換言之,咱們無法讓基本數據類型,或者解構後的變量(若是它的值也是基本數據類型的話),成爲響應式的數據。

// 咱們是永遠沒辦法讓`a`或`x`這樣的基本數據成爲響應式的數據的,Proxy也沒法劫持基本數據。
const a = 1;
const { x: 1 } = { x: 1 }
複製代碼

可是有時候,咱們確實就是想一個數字、一個字符串是響應式的,或者就是想利用解構的寫法。那怎麼辦呢?只能經過建立一個對象,也便是源碼中的Ref數據,而後將原始數據保存在Ref的屬性value當中,再將它的引用返回給使用者。既然是咱們本身創造出來的對象,也就不必使用Proxy再作代理了,直接劫持這個value的get/set便可,這就是ref函數與Ref類型的由來。

不過單靠ref還無法解決對象解構的問題,它只是將基本數據保持在一個對象的value中,以實現數據響應式。對於對象的解構還須要另一個函數:toRefs

export function toRefs<T extends object>( object: T ): { [K in keyof T]: Ref<T[K]> } {
  const ret: any = {}
  // 遍歷對象的全部key,將其值轉化爲Ref數據
  for (const key in object) {
    ret[key] = toProxyRef(object, key)
  }
  return ret
}
function toProxyRef<T extends object, K extends keyof T>( object: T, key: K ): Ref<T[K]> {
  const v = {
    [refSymbol]: true,
    get value() {
      // 注意,這裏沒用到track
      return object[key]
    },
    set value(newVal) {
      // 注意,這裏沒用到trigger
      object[key] = newVal
    }
  }
  return v as Ref<T[K]>
}
複製代碼

經過遍歷對象,將每一個屬性值都轉成Ref數據,這樣解構出來的仍是Ref數據,天然就保持了響應式數據的引用。可是源碼中有一點要注意,toRefs函數中引用的是toProxyRef而不是ref,它並不會在get/set中注入tracktrigger,也就是說,**向toRefs傳入一個正常的對象,是不會返回一個響應式的數據的。**必需要傳遞一個已經被reactive執行返回的對象纔能有響應式的效果。感受這點能夠優化,暫時也不知道小右這樣作的緣由是什麼。因爲這裏會牽扯到tracktrigger,而這兩個在我寫本文時還沒研究,就沒膽子提PR了。

到這,咱們就把ref的源碼給看完了。

下一章節咱們開始看reactive,它是核心,從它開始,內部的各個api開始真正的串連。


本文做者:螞蟻保險-體驗技術組-阿相

掘金地址:相學長

相關文章
相關標籤/搜索