Vue 3.x 源碼初探——reactive原理

近期 Vue 官方正式開放了 3.x 的源碼,目前處於Pre Alpha階段,筆者出於興趣,抽空對 Vue 3.x 源碼的數據響應式部分作了簡單閱讀。本文經過分析 Vue 3.x 的 reactive API 的原理,能夠更方便理解 Vue 3.x 比起 Vue 2.x 響應式原理的區別。javascript

在 Vue 3.x 源碼開放以前,筆者曾寫過Vue Composition API 響應式包裝對象原理, Vue 3.x 的 reactive API 的實現與之有相似,感興趣的同窗能夠結合前文進行閱讀。html

閱讀此文以前,若是對如下知識點不夠了解,能夠先了解如下知識點:vue

筆者以前也寫過相關文章,也能夠結合相關文章:java

搭建Vue 3.x 運行環境

進入vue-next的項目倉庫,咱們能夠把 Vue 3.x 項目代碼都clone下來,能夠看到,經過執行vue-next/scripts/build.js能夠將 Vue 3.x 的代碼使用 rollup 打包,生成一個名爲vue.global.js,可供開發者引用。爲了方便調試,咱們執行vue-next/scripts/dev.js,此時開啓 rollup 的 watch 模式,能夠方便咱們對源碼進行調試、修改、輸出。react

在項目目錄下新建一個test.html,引用構建在項目目錄下的packages/vue/dist/vue.global.js,在項目目錄下執行npm run dev,寫一個最簡單 Vue 3.x 的 demo ,用瀏覽器打開能夠直接運行,利用這個 demo ,咱們構建好了 Vue 3.x 基本的運行環境,下面能夠開始進行源碼的調試了。git

<!DOCTYPE html>
<html>
<head>
    <title>vue-demo</title>
</head>
<body>
    <div id="app"></div>
    <script src="./packages/vue/dist/vue.global.js"></script>
    <script> const { createComponent, createApp, reactive, toRefs } = Vue; const component = createComponent({ template: ` <div> {{ count }} <button @click="addHandler">add</button> </div> `, setup(props) { const data = reactive({ count: 0, }); const addHandler = () => { data.count++; }; return { ...toRefs(data), addHandler, }; }, }); createApp().mount(component, document.querySelector('#app')); </script>
</body>
</html>
複製代碼

Reactive源碼解析

打開vue-next/packages/reactivity/src/reactive.ts,首先能夠找到reactive函數以下:github

export function reactive(target: object) {
  // 若是是readonly對象的代理,那麼這個對象是不可觀察的,直接返回readonly對象的代理
  if (readonlyToRaw.has(target)) {
    return target
  }
  // 若是是readonly原始對象,那麼這個對象也是不可觀察的,直接返回readonly對象的代理,這裏使用readonly調用,能夠拿到readonly對象的代理
  if (readonlyValues.has(target)) {
    return readonly(target)
  }

  // 調用createReactiveObject建立reactive對象
  return createReactiveObject(
    target, // 目標對象
    rawToReactive, // 原始對象映射響應式對象的WeakMap
    reactiveToRaw, // 響應式對象映射原始對象的WeakMap
    mutableHandlers, // 響應式數據的代理handler,通常是Object和Array
    mutableCollectionHandlers // 響應式集合的代理handler,通常是Set、Map、WeakMap、WeakSet
  )
}
複製代碼

上面的代碼很好理解,調用reactive,首先進行是不是 readonly 對象的判斷,若是 target 對象是 readonly 對象或者經過調用Vue.readonly返回的代理對象,則是不可相應的,會直接返回 readonly 響應式代理對象。而後調用createReactiveObject建立響應式對象。算法

createReactiveObject傳遞的五個參數分別是:目標對象、原始對象映射響應式對象的WeakMap、響應式對象映射原始對象的WeakMap、響應式數據的代理handler,通常是Object和Array、響應式集合的代理handler,通常是Set、Map、WeakMap、WeakSet。咱們能夠翻到vue-next/packages/reactivity/src/reactive.ts最上方,能夠看到定義瞭如下常量:npm

// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()

// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
const readonlyValues = new WeakSet<any>()
const nonReactiveValues = new WeakSet<any>()

const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
複製代碼

能夠看到在reactive中會預存如下四個WeakMaprawToReactivereactiveToRawrawToReadonlyreadonlyToRaw,分別是原始對象到響應式對象和 readonly 代理對象到原始對象的相互映射,另外定義了readonlyValuesnonReactiveValues,分別是 readonly 代理對象的集合與調用Vue.markNonReactive標記爲不可相應對象的集合。collectionTypesSetMapWeakMapWeakSet的集合api

用 WeakMap 來進行相互映射的緣由是 WeakMap 的 key 是弱引用的。而且比起 Map , WeakMap 的賦值和搜索操做的算法複雜度均低於 Map ,具體緣由可查閱相關文檔

下面來看createReactiveObject

function createReactiveObject( target: unknown, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) {
  // 若是不是對象,直接返回,開發環境下會給警告
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // 目標對象已是可觀察的,直接返回已建立的響應式Proxy,toProxy就是rawToReactive這個WeakMap,用於映射響應式Proxy
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // 目標對象已是響應式Proxy,直接返回響應式Proxy,toRaw就是reactiveToRaw這個WeakMap,用於映射原始對象
  if (toRaw.has(target)) {
    return target
  }
  // 目標對象是不可觀察的,直接返回目標對象
  if (!canObserve(target)) {
    return target
  }
  // 下面是建立響應式代理的核心邏輯
  // Set、Map、WeakMap、WeakSet的響應式對象handler與Object和Array的響應式對象handler不一樣
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  // 建立Proxy
  observed = new Proxy(target, handlers)
  // 更新rawToReactive和reactiveToRaw映射
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  // 看reactive的源碼,targetMap的用處目前還不清楚,應該是做者預留的還沒有完善的feature而準備的
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}
複製代碼

看了上面的代碼,咱們知道createReactiveObject用於建立響應式代理對象:

  • 首先判斷target是不是對象類型,若是不是對象,直接返回,開發環境下會給警告
  • 而後判斷目標對象是否已是可觀察的,若是是,直接返回已建立的響應式Proxy,toProxy就是rawToReactive這個WeakMap,用於映射響應式Proxy
  • 而後判斷目標對象是否已是響應式Proxy,若是是,直接返回響應式Proxy,toRaw就是reactiveToRaw這個WeakMap,用於映射原始對象
  • 而後建立響應式代理,對於SetMapWeakMapWeakSet的響應式對象handler與ObjectArray的響應式對象handler不一樣,要分開處理
  • 最後更新rawToReactivereactiveToRaw映射

響應式代理陷阱

Object和Array的代理

下面的重心來到了分析mutableCollectionHandlersmutableHandlers,首先分析vue-next/packages/reactivity/src/baseHandlers.ts,這個handler用於建立Object類型和Array類型的響應式Proxy使用:

export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}
複製代碼

咱們知道,最重要的就是代理get陷阱和set陷阱,首先來看get陷阱:

function createGetter(isReadonly: boolean) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // 經過Reflect拿到原始的get行爲
    const res = Reflect.get(target, key, receiver)
    // 若是是內置方法,不須要另外進行代理
    if (isSymbol(key) && builtInSymbols.has(key)) {
      return res
    }
    // 若是是ref對象,代理到ref.value
    if (isRef(res)) {
      return res.value
    }
    // track用於收集依賴
    track(target, OperationTypes.GET, key)
    // 判斷是嵌套對象,若是是嵌套對象,須要另外處理
    // 若是是基本類型,直接返回代理到的值
    return isObject(res)
      // 這裏createGetter是建立響應式對象的,傳入的isReadonly是false
      // 若是是嵌套對象的狀況,經過遞歸調用reactive拿到結果
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}
複製代碼
  • get 陷阱首先經過Reflect.get,拿到原始的get行爲
  • 而後判斷若是是內置方法,不須要另外進行代理
  • 而後判斷若是是ref對象,代理到ref.value
  • 而後經過track來收集依賴
  • 最後判斷拿到的res結果是不是對象類型,若是是對象類型,再次調用reactive(res)來拿到結果,避免循環引用的狀況

下面來看set陷阱:

function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean {
  // 首先拿到原始值oldValue
  value = toRaw(value)
  const oldValue = (target as any)[key]
  // 若是原始值是ref對象,新賦值不是ref對象,直接修改ref包裝對象的value屬性
  if (isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
  // 原始對象裏是否有新賦值的這個key
  const hadKey = hasOwn(target, key)
  // 經過Reflect拿到原始的set行爲
  const result = Reflect.set(target, key, value, receiver)
  // don't trigger if target is something up in the prototype chain of original
  // 操做原型鏈的數據,不作任何觸發監聽函數的行爲
  if (target === toRaw(receiver)) {
    /* istanbul ignore else */
    if (__DEV__) {
      const extraInfo = { oldValue, newValue: value }
      // 沒有這個key,則是添加屬性
      // 不然是給原始屬性賦值
      // trigger 用於通知deps,通知依賴這一狀態的對象更新
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key, extraInfo)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key, extraInfo)
      }
    } else {
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key)
      }
    }
  }
  return result
}
複製代碼
  • set 陷阱首先拿到原始值oldValue
  • 而後進行判斷,若是原始值是ref對象,新賦值不是ref對象,直接修改ref包裝對象的value屬性
  • 而後經過Reflect拿到原始的set行爲,若是原始對象裏是否有新賦值的這個key,沒有這個key,則是添加屬性,不然是給原始屬性賦值
  • 進行對應的修改和添加屬性操做,經過調用trigger通知deps更新,通知依賴這一狀態的對象更新

Set、Map、WeakMap、WeakSet的代理

分析了mutableHandlers,下面來分析mutableCollectionHandlers,打開vue-next/packages/reactivity/src/collectionHandlers.ts,這個handler用於建立SetMapWeakMapWeakSet的響應式Proxy使用:

// 須要監聽的方法調用
const mutableInstrumentations: Record<string, Function> = {
  get(this: MapTypes, key: unknown) {
    return get(this, key, toReactive)
  },
  get size(this: IterableCollections) {
    return size(this)
  },
  has,
  add,
  set,
  delete: deleteEntry,
  clear,
  forEach: createForEach(false)
}

// ...


function createInstrumentationGetter( instrumentations: Record<string, Function> ) {
  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) =>
    // 若是是`get`、`has`、`add`、`set`、`delete`、`clear`、`forEach`的方法調用,或者是獲取`size`,那麼改成調用mutableInstrumentations裏的相關方法
    Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
}

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(mutableInstrumentations)
}
複製代碼

看上面的代碼,咱們看到mutableCollectionHandlers只有一個get陷阱,這是爲何呢?由於對於SetMapWeakMapWeakSet的內部機制的限制,其修改、刪除屬性的操做經過setadddelete等方法來完成,是不能經過Proxy設置set陷阱來監聽的,相似於 Vue 2.x 數組的變異方法的實現,經過監聽get陷阱裏的gethasaddsetdeleteclearforEach的方法調用,並攔截這個方法調用來實現響應式。

關於爲何SetMapWeakMapWeakSet不能作到響應式,筆者在why-is-set-incompatible-with-proxy找到了答案。

那麼咱們理解了由於Proxy對於SetMapWeakMapWeakSet的限制,與 Vue 2.x 的變異方法相似,經過攔截gethasaddsetdeleteclearforEach的方法調用來監聽SetMapWeakMapWeakSet數據類型的修改。看gethasaddsetdeleteclearforEach等方法就輕鬆多了,這些方法與對象類型的get陷阱、hasset等陷阱handler相似,筆者在這裏不作過多講述。

小結

本文是筆者處於繼續對 Vue 3.x 相關動態的關注,首先,筆者講述瞭如何搭建一個最簡單的 Vue 3.x 代碼的運行和調試環境,而後對 Vue 3.x 響應式核心原理進行解析,比起 Vue 2.x , Vue 3.x 對於響應式方面全面擁抱了 Proxy API,經過代理初始對象默認行爲來實現響應式;reactive內部利用WeakMap的弱引用性質和快速索引的特性,使用WeakMap保存了響應式代理和原始對象, readonly 代理和原始對象的互相映射;最後,筆者分析了響應式代理的相關陷阱方法,能夠知道對於對象和數組類型,是經過響應式代理的相關陷阱方法實現原始對象響應式,而對於SetMapWeakMapWeakSet類型,由於受到Proxy的限制,Vue 3.x 使用了劫持gethasaddsetdeleteclearforEach等方法調用來實現響應式原理。

相關文章
相關標籤/搜索