Vue 3 原理剖析:數據響應系統

這是個人剖析 Vue 3 原理的第一篇文章。這篇將會帶着你們學習數據響應相關的內容,而且儘量的脫離源碼來了解原理,下降你們的學習難度。vue

文章相關資料

Vue 3 目前的狀態其實很適合閱讀,由於代碼量很少,而且核心功能是不會有什麼大的變更的。react

所以筆者 fork 了目前的源碼,而且加以註釋。同時爲了照顧不怎麼熟悉 TS 的人羣,筆者也對一些核心的 TS 語法作了解釋。git

這份註釋不是乾巴巴的只對一行代碼說明是幹什麼的,而是結合了上下文來說解它的用處。若是你想讀源碼可是又怕看不懂的話,能夠經過我這個 倉庫 來學習。github

先導知識

Vue 3 代碼的寫法有了很大的變化,若是你還不清楚這方面的內容,推薦先閱讀 Vue Function-based API RFC數組

數據響應機制

衆所周知,在 Vue 3 中使用了 Proxy 替換了原先的 Object.defineproperty 來實現數據響應。數據結構

另外若是你不熟悉 Proxy 的用法,推薦先閱讀 文檔函數

咱們先來學習下如何使用這個 API 吧。性能

const value = reactive({ num: 0 })
// 須要注意的一點,這個回調中用到了 value.num
// 那麼只有當外部給 value.num 賦值纔會觸發回調
effect(() => {
  console.log(value.num)
})
value.num = 7
複製代碼

很簡單,上述代碼就實現了數據的響應式,而且能在數據改變之後執行相應的回調。學習

reactive 內部的核心代碼簡化以下:ui

function reactive(target) {
    if (!isObject(target)) {
        return target
    }
    if (!canObserve(target)) {
        return target
    }
    const handlers = collectionTypes.has(target.constructor)
        ? collectionHandlers
        : baseHandlers
    observed = new Proxy(target, handlers)
    return observed
}
複製代碼

首先判斷傳入的參數類型是否能夠用於觀察,目前支持的類型爲 Object|Array|Map|Set|WeakMap|WeakSet

接下來判斷參數的構造函數,根據類型得到不一樣的 handlers。這裏咱們就統一使用 baseHandlers,由於這個已經覆蓋 99% 的狀況了。只有 Set, Map, WeakMap, WeakSet 纔會使用到 collectionHandlers

對於 baseHandlers 來講,最主要的是劫持了 getset 行爲,這兩個行爲同時也能原生劫持數組下標修改值及對象新增屬性的行爲,這兩個行爲相關的內容會在下文中說到。

最後就是構造一個 Proxy 對象完成數據的響應式。相比 Object.defineproperty 一開始就要遞歸遍歷整個對象的作法來講,使用 Proxy 性能會好得多。

接下來當咱們去使用 value 這個對象的時候,就能劫持到內部的行爲。

好比說 console.log(value.num) 就會觸發 get 函數;value.num = 2 就會觸發 set 函數。

如下是這兩個函數的核心剖析:

function get(target: any, key: string | symbol, receiver: any) {
  // 得到結果
  const res = Reflect.get(target, key, receiver)
  track(target, OperationTypes.GET, key)
  // 判斷是否爲對象,是的話將對象包裝成 proxy
  return isObject(res) ? reactive(res) : res
}
複製代碼

對於 get 函數來講,獲取值確定是最核心的一步驟了。接下來是調用 track,這個和 effect 有關,下文再說。最後是判斷值的類型,若是是對象的話就繼續包裝成 Proxy

function set( target: any, key: string | symbol, value: any, receiver: any ): boolean {
  const result = Reflect.set(target, key, value, receiver)
  if (是否新增 key) {
    trigger(target, OperationTypes.ADD, key)
  } else if (value !== oldValue) {
    trigger(target, OperationTypes.SET, key)
  }  
  return result
}
複製代碼

對於 set 函數來講,設置值是第一步驟,而後調用 trigger,這也是 effect 中的內容。

簡單來講,若是某個 effct 回調中有使用到 value.num,那麼這個回調會被收集起來,並在調用 value.num = 2 時觸發。

那麼怎麼收集這些內容呢?這就要說說 targetMap 這個對象了。它用於存儲依賴關係,相似如下結構,這個結構會在 effect 文件中被用到

{
  target: {
    key: Dep
  }
}
複製代碼

先來解釋下三者究竟是什麼,這個很重要

  • target 就是被 proxy 的對象
  • key 是對象觸發 get 行爲之後的屬性。好比 counter.num 觸發了 get 行爲,num 就是 key
  • dep 是回調函數,也就是 effect 中調用了 counter.num 的話,這個回調就是 dep,須要收集起來下次使用

這裏筆者把這些內容脫離源碼串起來說一下流程。

const counter = reactive({ num: 0 })
effect(() => {
  console.log(counter.num)
})
counter.num = 7
複製代碼

首先建立一個 Proxy 對象,targetMap 會把這個對象收集起來當作 key。

接下來調用 effect 回調的時候會把這個回調保存起來,用於下面的依賴收集。在調用的過程當中會觸發 counterget 函數,內部調用了 track 函數,這個函數會使用到 targetMap

這裏首先經過 targettargetMap 中取到一個對象,這個對象也就是 target 全部的依賴關係。那麼對於 counter.num 來講,num 就是這個對象的 key(這裏若是有點模糊的話能夠先看下上面的數據結構),值是一個依賴回調的集合,由於 counter.num 可能會被多個地方依賴到。

回調執行完畢之後會把保存的回調銷燬掉。

當咱們調用 counter.num = 7 時,觸發 set 函數,內部調用 trigger 函數,一樣會使用到 targetMap

一樣經過 target 取到一個對象,而後經過 key 也就是 num 去取出依賴集合,最後遍歷這個集合執行裏面全部的回調函數。

另外對於 computed 來講,內部也是使用到了 effect,無非它的回調不會在調用 effect 後當即執行,只有當觸發 get 行爲之後纔會執行回調並進行依賴收集,舉個例子:

const value = reactive({ num: 0 })
const cValue = computed(() => value.num)
value.num = 1
複製代碼

對於以上代碼來講,computed 的回調永遠不會執行,只有當使用到了 cValue.value 時纔會執行回調,而後接下來的操做就和上面的沒區別了。

最後

以上是數據響應核心流程的講解,內容很少,可是你通讀源碼之後也就是這樣一個流程。

若是你對源碼有興趣的話,就結合我這個 倉庫 來對照這篇文章吧。

閱讀源碼是一個很枯燥的過程,可是收益也是巨大的。若是你在閱讀的過程當中有任何的問題,都歡迎你在評論區與我交流。

另外寫這系列是個很耗時的工程,須要維護代碼註釋,還得把文章寫得儘可能讓讀者看懂,最後還得配上畫圖,若是你以爲文章看着還行,就請不要吝嗇你的點贊。

最後,若是你對源碼研究也有興趣或者有問題想問的,能夠進羣交流。

相關文章
相關標籤/搜索