vue3響應式源碼解析-Reactive篇

前言

爲了更好的作解釋,我會調整源碼中的接口、類型、函數等聲明順序,並會增長一些註釋方便閱讀javascript

在上一章中,咱們介紹了ref,若是仔細看過,想必對ref應該已經瞭如指掌了。若是尚未,或着忘記了....能夠先回顧一下上篇文章。html

閱讀本篇文章須要的前置知識有:vue

  1. Proxy
  2. WeakMap
  3. Reflect

Reactive

reactive這個文件代碼其實很少,100 來行,不少邏輯實際上是在handlerseffect中。咱們先看這個文件的引入:java

外部引用

import {
  isObject, // 判斷是不是對象
  toTypeString // 獲取數據的類型名稱
} from '@vue/shared'
// 此處的handles最終會傳遞給Proxy(target, handle)的第二個參數
import {
  mutableHandlers, // 可變數據代理處理
  readonlyHandlers // 只讀(不可變)數據代理處理
} from './baseHandlers'

// collections 指 Set, Map, WeakMap, WeakSet
import {
  mutableCollectionHandlers, // 可變集合數據代理處理
  readonlyCollectionHandlers // 只讀集合數據代理處理
} from './collectionHandlers'

// 上篇文章中說了半天的泛型類型
import { UnwrapRef } from './ref'
// 看過單測篇的話,應該知道這個是被effect執行後返回的監聽函數的類型
import { ReactiveEffect } from './effect'
複製代碼

因此不用怕,不少只是引了簡單的工具方法跟類型,真正跟外部函數有關聯的就是幾個handlersreact

類型與常量

再來看類型的聲明跟變量的聲明,先看註釋不少的targetMaptypescript

// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead.
export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<string | symbol, Dep>
// 翻譯自上述英文:利用WeakMap是爲了更好的減小內存開銷。
export const targetMap = new WeakMap<any, KeyToDepMap>()
複製代碼

traget的意思就是Proxy(target, handle)函數的第一個入參,也就是咱們想轉成響應式數據的原始數據。但這個KeyToDepMap其實看不明白具體是怎麼樣的映射。先放着,等到咱們真正使用它時,再來看。api

繼續往下看,是一堆常量的聲明。數組

// raw這個單詞在ref篇咱們見過,它在這個庫的含義是,原始數據
// 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])
// 用於正則判斷是否符合可觀察數據,object + array + collectionTypes
const observableValueRE = /^\[object (?:Object|Array|Map|Set|WeakMap|WeakSet)\]$/
複製代碼

若是讀過單測篇(reactive 的第 八、九、10 個單測),可能會記得以前說過,內部須要兩個WeakMap來實現原始數據跟響應數據的雙向映射。明顯的rawToReactivereactiveToRaw就是這兩個WeakMaprawToReadonlyreadonlyToRaw顧名思義的就是映射原始數據跟只讀的響應數據的兩個WeakMapapp

readonlyValuesnonReactiveValues根據註釋以及以前單測篇的記憶,多是跟markNonReactivemarkReadonly(這個單測篇沒講到)有關。猜想是用來存儲用這兩個 api 構建的數據,具體也能夠後面再看。dom

collectionTypesobservableValueRE看註釋便可。

工具函數

在真正看reactive以前,咱們把本文件內部的一些工具方案先過一遍,這樣看源碼時就不會東跳西跳比較亂。這部分比較簡單,簡單瞄兩眼就行了。

// 數據是否可觀察
const canObserve = (value: any): boolean => {
  return (
    // 整個vue3庫都沒搜到_isVue的邏輯,猜想是vue組件,不影響本庫閱讀
    !value._isVue &&
    // 虛擬dom的節點不可觀察
    !value._isVNode &&
    // 屬於可觀察的數據類型
    observableValueRE.test(toTypeString(value)) &&
    // 該集合中存儲的數據不可觀察
    !nonReactiveValues.has(value)
  )
}

// 若是reactiveToRaw或readonlyToRaw中存在該數據了,說明就是響應式數據
export function isReactive(value: any): boolean {
  return reactiveToRaw.has(value) || readonlyToRaw.has(value)
}

// 判斷是不是隻讀的響應式數據
export function isReadonly(value: any): boolean {
  return readonlyToRaw.has(value)
}

// 將響應式數據轉爲原始數據,若是不是響應數據,則返回源數據
export function toRaw<T>(observed: T): T {
  return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed
}

// 傳遞數據,將其添加到只讀數據集合中
// 注意readonlyValues是個WeakSet,利用set的元素惟一性,能夠避免重複添加
export function markReadonly<T>(value: T): T {
  readonlyValues.add(value)
  return value
}

// 傳遞數據,將其添加至不可響應數據集合中
export function markNonReactive<T>(value: T): T {
  nonReactiveValues.add(value)
  return value
}
複製代碼

核心實現

上述的代碼都是佐料,下面看本文件的核心代碼,首先看reactivereadonly函數

// 函數類型聲明,接受一個對象,返回不會深度嵌套的Ref數據
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
// 函數實現
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  // 若是傳遞的是一個只讀響應式數據,則直接返回,這裏其實能夠直接用isReadonly
  if (readonlyToRaw.has(target)) {
    return target
  }
  // target is explicitly marked as readonly by user
  // 若是是被用戶標記的只讀數據,那經過readonly函數去封裝
  if (readonlyValues.has(target)) {
    return readonly(target)
  }

  // 到這一步的target,能夠保證爲非只讀數據

  // 經過該方法,建立響應式對象數據
  return createReactiveObject(
    target, // 原始數據
    rawToReactive, // 原始數據 -> 響應式數據映射
    reactiveToRaw, // 響應式數據 -> 原始數據映射
    mutableHandlers, // 可變數據的代理劫持方法
    mutableCollectionHandlers // 可變集合數據的代理劫持方法
  )
}

// 函數聲明+實現,接受一個對象,返回一個只讀的響應式數據。
export function readonly<T extends object>(
  target: T
): Readonly<UnwrapNestedRefs<T>> {
  // value is a mutable observable, retrieve its original and return
  // a readonly version.
  // 若是自己是響應式數據,獲取其原始數據,並將target入參賦值爲原始數據
  if (reactiveToRaw.has(target)) {
    target = reactiveToRaw.get(target)
  }
  // 建立響應式數據
  return createReactiveObject(
    target,
    rawToReadonly,
    readonlyToRaw,
    readonlyHandlers,
    readonlyCollectionHandlers
  )
}
複製代碼

兩個方法代碼其實很簡單,主要邏輯都封裝到了createReactiveObject,兩個方法的主要做用是:

  1. 透傳給createReactiveObject相應地的代理數據與響應式數據的雙向映射 map。
  2. reactive會作readonly的相關校驗,反之readonly方法也是。

下面繼續看:

function createReactiveObject( target: any, 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
  }
  // 經過原始數據 -> 響應數據的映射,獲取響應數據
  let observed = toProxy.get(target)
  // target already has corresponding Proxy
  // 若是原始數據已是響應式數據,則直接返回此響應數據
  if (observed !== void 0) {
    return observed
  }
  // target is already a Proxy
  // 若是原始數據自己就是個響應數據了,直接返回自身
  if (toRaw.has(target)) {
    return target
  }
  // only a whitelist of value types can be observed.
  // 若是是不可觀察的對象,則直接返回原對象
  if (!canObserve(target)) {
    return target
  }
  // 集合數據與(對象/數組) 兩種數據的代理處理方式不一樣。
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  // 聲明一個代理對象,也便是響應式數據
  observed = new Proxy(target, handlers)
  // 設置好原始數據與響應式數據的雙向映射
  toProxy.set(target, observed)
  toRaw.set(observed, target)

  // 在這裏用到了targetMap,可是它的value值存放什麼咱們依舊不知道
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}
複製代碼

咱們能夠看到一些細節:

  1. 若是傳遞的是非對象,只是開發環境報警告,並不會致使異常。這是由於生產環境極其複雜,因爲 js 是一門動態語言,若是直接報錯,一定直接影響各種線上應用。在這只是返回了原始數據,失去了響應性,但不會致使真實頁面異常。
  2. 這個方法基本沒有 TS 類型了。

reactive文件其實很是通俗易懂,看完之後,咱們心中只有 2 個問題:

  1. baseHandlerscollectionHandlers的具體實現以及爲何要區分?
  2. targetMap究竟是啥?

固然咱們知道handlers確定是作依賴收集跟響應觸發的。那咱們就先看着兩個文件。

baseHandles

打開此文件,一樣先看外部引用:

// 這些咱們已經瞭解了
import { reactive, readonly, toRaw } from './reactive'
import { isRef } from './ref'
// 這些就是些工具方法,hasOwn 意爲對象是否擁有某數據
import { isObject, hasOwn, isSymbol } from '@vue/shared'
// 這裏定義了操做數據的行爲枚舉
import { OperationTypes } from './operations'
// LOCKED:global immutability lock
// 一個全局用來判斷是否數據是不可變的開關
import { LOCKED } from './lock'
// 收集依賴跟觸發監聽函數的兩個方法
import { track, trigger } from './effect'
複製代碼

只有tracktrigger的內部實現咱們不知道,其餘的要麼已經瞭解了,要麼點開看看一眼就明白。

而後是一個表明 JS 內部語言行爲的描述符的集合,不明白的能夠看相應MDN。具體怎麼使用能夠後面再看。

const builtInSymbols = new Set(
  Object.getOwnPropertyNames(Symbol)
    .map(key => Symbol[key])
    .filter(key => typeof key === 'symbol')
)
複製代碼

而後會發現下面就百來行代碼,咱們找到reactive中引用的mutableHandlersreadonlyHandlers。咱們先看簡單的mutableHandlers

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

這是一個ProxyHandle,關於Proxy若是忘記了,記得再看一遍MDN

而後終於到了整個響應式系統最關鍵的地方了,這五個trapsget,set,deleteProperty,has,ownKeys。固然Proxy能實現的trap並不只是這五個。其中definePropertygetOwnPropertyDescriptor兩個trap不涉及響應式,不須要劫持。還有一個enumerate已經被廢棄。enumerate本來會劫持for-in的操做的,那你會想,那這個廢棄了,咱們的for-in怎麼辦?放心,它仍是走到ownKeys這個trap,進而觸發咱們的監聽函數的。

說遠了,回到代碼中,咱們從負責收集依賴的get看。這個trap是經過createGetter函數生成,那咱們來看看它。

get

createGetter接受一個入參:isReadonly。那天然在readonlyHandlers中就是傳true

// 入參只有一個是否只讀
function createGetter(isReadonly: boolean) {
  // 關於proxy的get,請閱讀:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get
  // receiver便是被建立出來的代理對象
  return function get(target: any, key: string | symbol, receiver: any) {
    // 若是還不瞭解Reflect,建議先閱讀它的文檔:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
    // 獲取原始數據的相應值
    const res = Reflect.get(target, key, receiver)
    // 若是是js的內置方法,不作依賴收集
    if (isSymbol(key) && builtInSymbols.has(key)) {
      return res
    }
    // 若是是Ref類型數據,說明已經被收集過依賴,不作依賴收集,直接返回其value值。
    if (isRef(res)) {
      return res.value
    }
    // 收集依賴
    track(target, OperationTypes.GET, key)
    // 經過get獲取的值不是對象的話,則直接返回便可
    // 不然,根據isReadyonly返回響應數據
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
複製代碼

大體看下來會發現,get的方法的每一個表達式其實都比較簡單,不過卻好像都有點兒懵。

問題 1: 爲何要經過Reflect, 而不是直接target[key]?

確實,target[key]好像就能實現效果了,爲何要用Reflect,還要傳個receiver呢?緣由在於原始數據的get並無你們想的這麼簡單,好比這種狀況:

const targetObj = {
  get a() {
    return this
  }
}
const proxyObj = reactive(targetObj)
複製代碼

這個時候,proxyObj.a在你想象中應該是proxyObj仍是targetObj呢?我以爲合理來講應該是proxyObj。但a又不是一個方法,無法直接call/apply。要實現也能夠,比較繞,約等於實現了Reflect的 polyfill。因此感謝 ES6,利用Reflect,可方便的把現有操做行爲原模原樣地反射到目標對象上,又保證真實的做用域(經過第三個參數receiver)。這個receiver便是生成的代理對象,在上述例子中便是proxyObj

問題 2: 爲何內置方法不須要收集依賴?

若是一個監聽函數是這樣的:

const origin = {
  a() {}
}
const observed = reactive(origin)
effect(() => {
  console.log(observed.a.toString())
})
複製代碼

很明顯,當origin.a 變化時,observed.a.toString()也是應該會變的,那爲何不用監聽了呢?很簡單,由於已經走到了observed.a.toString()已經走了一次get的 trap,不必重複收集依賴。故而相似的內置方法,直接 return。

問題 3: 爲何屬性值爲對象時,須要再用reactive|readonly執行?

註釋中寫了:

need to lazy access readonly and reactive here to avoid circular dependency

翻譯成普通話是,須要延遲地使用reactive|readonly來避免循環依賴。這話須要品,細細品,品了一下子之後終於品懂了。

由於因爲Proxy這玩意兒吧,它的trap其實只能劫持對象的第一層訪問與更新。若是是嵌套對象,實際上是劫持不了的。那咱們就有了兩種方法:

方法一:當經過reactive|readonly轉化原始對象時,一層一層的遞歸解套,若是是對象,就再用reactive執行、而後走ProxyHandle。之後訪問這些嵌套屬性時,天然也會走到 trap。但這樣有個大問題,若是對象是循環引用的呢?那必然是要有個邏輯判斷,若是發現屬性值是自身則不遞歸了。那若是是半路循環引用的呢?好比這樣:

const a = {
  b: {
    c: a
  }
}

const A = {
  B: {
    C: a
  }
}
複製代碼

想一想都頭大吧。

方法二:也便是源碼中的方法,轉化原始對象時,不遞歸。後續走到get的 trap 時,若是發現屬性值是個對象,再繼續轉化、劫持。也就是註釋中所講到的lazy。利用這個辦法,天然就能夠避免循環引用了。另外還有個顯而易見的好處是,能夠優化性能。

除了這個三個問題外,還有一個小細節:

if (isRef(res)) {
  return res.value
}
複製代碼

若是是Ref類型的數據,則直接返回 value 值。由於在ref函數中,已經作了相關的依賴跟蹤邏輯。另外,若是看過單測篇跟 ref 篇,咱們知道就是此處代碼實現了這樣的能力:向reacitive函數傳遞一個嵌套的Ref類型數據,可返回一個遞歸解套了Ref類型的響應式數據。reactive函數的返回類型爲UnwrapNestedRefs歸功於此。

不過切記:向reactive傳一個純粹的Ref類型數據,是不會解套的,它只解套被嵌套着的Ref數據。示例以下:

reactive(ref(4)) // = ref(4);
reactive({ a: ref(4) }) // = { a: 4 }
複製代碼

那到此爲止,除了track是外部引入的用來收集依賴的方法外(後面再看),get已經摸透了。

下面看set

set

function set( target: any, key: string | symbol, value: any, receiver: any ): boolean {
  // 若是value是響應式數據,則返回其映射的源數據
  value = toRaw(value)
  // 獲取舊值
  const oldValue = target[key]
  // 若是舊值是Ref數據,但新值不是,那更新舊的值的value屬性值,返回更新成功
  if (isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
  // 代理對象中,是否是真的有這個key,沒有說明操做是新增
  const hadKey = hasOwn(target, key)
  // 將本次設置行爲,反射到原始對象上
  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 是個單測覆蓋率工具
    /* istanbul ignore else */
    if (__DEV__) {
      // 開發環境下,會傳給trigger一個擴展數據,包含了新舊值。明顯的是便於開發環境下作一些調試。
      const extraInfo = { oldValue, newValue: value }
      // 若是不存在key時,說明是新增屬性,操做類型爲ADD
      // 存在key,則說明爲更新操做,當新值與舊值不相等時,纔是真正的更新,進而觸發trigger
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key, extraInfo)
      } else if (value !== oldValue) {
        trigger(target, OperationTypes.SET, key, extraInfo)
      }
    } else {
      // 同上述邏輯,只是少了extraInfo
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key)
      } else if (value !== oldValue) {
        trigger(target, OperationTypes.SET, key)
      }
    }
  }
  return result
}
複製代碼

setget同樣,每句表達式都很清晰,但咱們依舊存在疑問。

問題 1:isRef(oldValue) && !isRef(value)這段是什麼邏輯?

// 若是舊值是 Ref 數據,但新值不是,那更新舊的值的 value 屬性值,返回更新成功
if (isRef(oldValue) && !isRef(value)) {
  oldValue.value = value
  return true
}
複製代碼

什麼狀況下 oldValue 會是個Ref數據呢?其實看get部分的時候,咱們就知道啦,reactive有解套嵌套 ref 數據的能力,如:

const a = {
  b: ref(1)
}
const observed = reactive(a) // { b: 1 }
複製代碼

此時,observed.b輸出的是 1,當作賦值操做 observed.b = 2時。oldValue因爲是a.b,是一個Ref類型數據,而新的值並非,進而直接修改a.b的 value 便可。那爲何直接返回,不須要往下觸發 trigger 了呢?是由於在ref函數中,已經有劫持 set 的邏輯了(不貼代碼了)。

問題 2:何時會target !== toRaw(receiver)

在以前的認知中,receiver有點兒像是this同樣的存在,指代着被 Proxy 執行後的代理對象。那代理對象用toRaw轉化,也就是轉爲原始對象,天然跟target是全等的。這裏就涉及了一個偏門的知識點,詳細介紹能夠看MDN。其中有說到:

Receiver:最初被調用的對象。一般是 proxy 自己,但 handler 的 set 方法也有可能在原型鏈上或以其餘方式被間接地調用(所以不必定是 proxy 自己)

這就是代碼中的註釋背後的意義:

don't trigger if target is something up in the prototype chain of original.

舉個實例來講就像這樣:

const child = new Proxy(
  {},
  {
    get(target, key, receiver) {
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver)
      console.log('child', receiver)
      return true
    }
  }
)

const parent = new Proxy(
  { a: 10 },
  {
    get(target, key, receiver) {
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver)
      console.log('parent', receiver)
      return true
    }
  }
)

Object.setPrototypeOf(child, parent)

child.a = 4

// 打印結果
// parent Proxy {child: true, a: 4}
// Proxy {child: true, a: 4}
複製代碼

在這種狀況下,這個父對象parentset居然也會被觸發一次,只不過傳遞的receiver都是child,進而被更改數據的也一直是child。在這種狀況下,parent其實並無變動,按道理來講,它確實不該該觸發它的監聽函數。

問題 3: 數組可能經過方法更新數據,這過程的監聽邏輯是怎麼樣的?

對於一個對象來講,咱們能夠直接賦值屬性值,但對於數組呢?假使const arr = [],那它既能夠arr[0] = 'value',也能夠arr.push('value'),但並無一個trap是劫持 push 的。可是當你真正去調試時,發現push還會觸發兩次set

const proxy = new Proxy([], {
  set(target, key, value, receiver) {
    console.log(key, value, target[key])
    return Reflect.set(target, key, value, receiver)
  }
})
proxy.push(1)
// 0 1 undefined
// length 1 1
複製代碼

其實push的內部邏輯就是先給下標賦值,而後設置length,觸發了兩次set。不過還有個現象是,雖然push帶來的length操做會觸發兩次set,但走到 length 邏輯時,獲取老的 length 也已是新的值了,因此因爲value === oldValue,實際只會走到一次trigger。可是!若是是shiftunshift,這樣的邏輯又不成立了,並且若是數組長度是 N,shift|unshift就會帶來 N 次的trigger。這裏其實涉及了Array的底層實現與規範,我也沒法簡單的闡述明白,建議能夠本身去看ECMA-262中關於Array的相關標準。

不過這裏確實留下一個小坑,shift|unshift以及splice,會帶來屢次的 effect 觸發。在reacivity系統中,目前還沒看到相關的優化。固然,真實在使用 vue@3 的過程當中,runtime-core仍是會針對渲染作批量更新的。

那到這,set自己的邏輯咱們也摸透了,除了一個外部引入的trigger。不過咱們知道它是當數據變動時觸發監聽函數的就好,後面再看。

接下來就比較簡單了。

其餘 traps

// 劫持屬性刪除
function deleteProperty(target: any, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = target[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    /* istanbul ignore else */
    if (__DEV__) {
      trigger(target, OperationTypes.DELETE, key, { oldValue })
    } else {
      trigger(target, OperationTypes.DELETE, key)
    }
  }
  return result
}
// 劫持 in 操做符
function has(target: any, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  track(target, OperationTypes.HAS, key)
  return result
}
// 劫持 Object.keys
function ownKeys(target: any): (string | number | symbol)[] {
  track(target, OperationTypes.ITERATE)
  return Reflect.ownKeys(target)
}
複製代碼

這幾個trap基本沒啥難點了,一眼能看明白。

最後看下readonly的特殊邏輯:

readonly

export const readonlyHandlers: ProxyHandler<any> = {
  // 建立get的trap
  get: createGetter(true),
  // set的trap
  set(target: any, key: string | symbol, value: any, receiver: any): boolean {
    if (LOCKED) {
      // 開發環境操做只讀數據報警告。
      if (__DEV__) {
        console.warn(
          `Set operation on key "${String(key)}" failed: target is readonly.`,
          target
        )
      }
      return true
    } else {
      // 若是不可變開關已關閉,則容許設置數據變動
      return set(target, key, value, receiver)
    }
  },
  // delete的trap,邏輯跟set差很少
  deleteProperty(target: any, key: string | symbol): boolean {
    if (LOCKED) {
      if (__DEV__) {
        console.warn(
          `Delete operation on key "${String( key )}" failed: target is readonly.`,
          target
        )
      }
      return true
    } else {
      return deleteProperty(target, key)
    }
  },
  has,
  ownKeys
}
複製代碼

readonly也很簡單啦,createGetter的邏輯以前已經看過了。不過有些沒繞過來的同窗可能會想,get的 trap 又不改變數據,爲何要跟reactive的作區分,傳個isReadonly呢?那是由於上文中講到的,經過get作依賴收集時,對於嵌套的對象數據,是延遲劫持的,因此只能透傳了isReadonly,讓後續劫持的子對象知道自身是否應該只讀。

hasownKeys因爲不改變數據,也不用遞歸收集依賴,天然就不用跟可變數據的邏輯作區分了。

看完之後,依賴收集跟觸發監聽函數的時機,咱們就能基本瞭解了。

小總結

關於 baseHandles 咱們作個小總結:

  1. 對於原始對象數據,會經過 Proxy 劫持,返回新的響應式數據(代理數據)。
  2. 對於代理數據的任何讀寫操做,都會經過Refelct反射到原始對象上。
  3. 在這個過程當中,對於讀操做,會執行收集依賴的邏輯。對於寫操做,會觸發監聽函數的邏輯。

總結下來,其實仍是比較簡單的。可是咱們還落了對於集合數據的 handlers 沒看,這塊纔是真正的硬骨頭。

collectionHandlers

打開這個文件,發現這個文件比reactivebaseHandlers可長了很多。沒想到對於這種日常不怎麼用的數據類型的處理,纔是最麻煩的。

爲何單獨處理

看源碼前,其實會有個疑問,爲何Set|Map|WeakMap|WeakSet這幾個數據須要特殊處理呢?跟其餘數據有什麼區別嗎?咱們點開文件,看看這個handlers,發現居然是這樣:

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

只有get,沒有sethas這些。這就懵了,說好的劫持setget呢?爲何不劫持set了?緣由是無法這麼作,咱們能夠簡單的作個嘗試:

const set = new Set([1, 2, 3])
const proxy = new Proxy(set, {
  get(target, key, receiver) {
    console.log(target, key, receiver)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log(target, key, value, receiver)
    return Reflect.set(target, key, value, receiver)
  }
})
proxy.add(4)
複製代碼

這段代碼,一跑就會出一個錯誤:

Uncaught TypeError: Method Set.prototype.add called on incompatible receiver [object Object]

發現只要劫持set,或者直接引入了Reflect,反射行爲到target上,就會報錯。爲何會這樣呢?這其實也是跟Map|Set這些的內部實現有關,他們內部存儲的數據必須經過this來訪問,被成爲所謂的「internal slots」,而經過代理對象去操做時,this實際上是 proxy,並非 set,因而沒法訪問其內部數據,而數組呢,因爲一些歷史緣由,又是能夠的。詳細解釋能夠見這篇關於 Proxy 的限制的介紹。這篇文章中也提到了解決辦法:

let map = new Map()
let proxy = new Proxy(map, {
  get(target, prop, receiver) {
    let value = Reflect.get(...arguments)
    return typeof value == 'function' ? value.bind(target) : value
  }
})
proxy.set('test', 1)
複製代碼

大體原理就是,當獲取的是一個函數的時候,將this綁定爲原始對象,也便是想要劫持的map|set。這樣就避免了this的指向問題。

那咱們就有點兒明白,爲何collection數據須要特殊處理,只劫持一個get了。具體怎麼作呢?咱們來看代碼。

按慣例先看引用與工具方法:

import { toRaw, reactive, readonly } from './reactive'
import { track, trigger } from './effect'
import { OperationTypes } from './operations'
import { LOCKED } from './lock'
import {
  isObject,
  capitalize, // 首字母轉成大寫
  hasOwn
} from '@vue/shared'

// 將數據轉爲reactive數據,若是不是對象,則直接返回自身
const toReactive = (value: any) => (isObject(value) ? reactive(value) : value)
const toReadonly = (value: any) => (isObject(value) ? readonly(value) : value)
複製代碼

這些引用,咱們應該基本不用看註釋都能明白了,除了一個工具方法capitalize須要點開看看外,基本一眼明白。而後咱們須要調整下閱讀順序,先大體看看到底如何經過一個gettrap,劫持寫操做。

插樁

// proxy handlers
export const mutableCollectionHandlers: ProxyHandler<any> = {
  // 建立一個插樁getter
  get: createInstrumentationGetter(mutableInstrumentations)
}
複製代碼

首先,咱們要讀懂它的函數名,createInstrumentationGetter。唔,像我同樣英文比較差的同窗多是不太懂Instrumentation是什麼意思的。這裏是表達「插樁」的意思。關於「插樁」我很少介紹啦,常見的單測覆蓋率每每就是經過插樁實現的。

在本代碼中,插樁即指向某個方法被注入一段有其餘做用的代碼,目的就是爲了劫持這些方法,增長相應邏輯,那咱們看看此處是如何「插樁」(劫持)的。

// 可變數據插樁對象,以及一系列相應的插樁方法
const mutableInstrumentations: any = {
  get(key: any) {
    return get(this, key, toReactive)
  },
  get size() {
    return size(this)
  },
  has,
  add,
  set,
  delete: deleteEntry,
  clear,
  forEach: createForEach(false)
}
// 迭代器相關的方法
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
  mutableInstrumentations[method] = createIterableMethod(method, false)
  readonlyInstrumentations[method] = createIterableMethod(method, true)
})
// 建立getter的函數
function createInstrumentationGetter(instrumentations: any) {
  // 返回一個被插樁後的get
  return function getInstrumented( target: any, key: string | symbol, receiver: any ) {
    // 若是有插樁對象中有此key,且目標對象也有此key,
    // 那就用這個插樁對象作反射get的對象,不然用原始對象
    target =
      hasOwn(instrumentations, key) && key in target ? instrumentations : target
    return Reflect.get(target, key, receiver)
  }
}
複製代碼

從上文中知道,因爲Proxycollection數據的原生特性,沒法劫持set或者直接反射。因此在這裏,建立了一個新的對象,它具備setmap同樣的方法名。這些方法名對應的方法就是插樁後,注入了依賴收集跟響應觸發的方法。而後經過Reflect反射到這個插樁對象上,獲取的是插樁後的數據,調用的是插樁後的方法。

而對於一些自定義的屬性或方法,Reflect反射的就不是插樁事後的,而是原數據,對於這些狀況,也不會作響應式的邏輯,好比單測中的:

it('should not observe custom property mutations', () => {
  let dummy
  const map: any = reactive(new Map())
  effect(() => (dummy = map.customProp))

  expect(dummy).toBe(undefined)
  map.customProp = 'Hello World'
  expect(dummy).toBe(undefined)
})
複製代碼

插樁讀操做

接下來看這個插裝器mutableInstrumentations,從上往下,咱們先看get

const mutableInstrumentations: any = {
  get(key: any) {
    // this 上述Reflect.get(target, key, receiver)中的target,也便是原始數據
    // toReactive是一個將數據轉爲響應式數據的方法
    return get(this, key, toReactive)
  }
  // ...省略其餘
}
function get(target: any, key: any, wrap: (t: any) => any): any {
  // 獲取原始數據
  target = toRaw(target)
  // 因爲Map能夠用對象作key,因此key也有多是個響應式數據,先轉爲原始數據
  key = toRaw(key)
  // 獲取原始數據的原型對象
  const proto: any = Reflect.getPrototypeOf(target)
  // 收集依賴
  track(target, OperationTypes.GET, key)
  // 使用原型方法,經過原始數據去得到該key的值。
  const res = proto.get.call(target, key)
  // wrap 即傳入的toReceive方法,將獲取的value值轉爲響應式數據
  return wrap(res)
}
複製代碼

注意:在get方法中,第一個入參target不能跟Proxy構造函數的第一個入參混淆。Proxy函數的第一個入參target指的原始數據。而在get方法中,這個target實際上是被代理後的數據。也便是Reflect.get(target, key, receiver)中的receiver

而後咱們就比較清晰了,本質就是經過原始數據的原型方法+call this,避免了上述的問題,返回真正的數據。

const mutableInstrumentations: any = {
  // ...
  get size() {
    return size(this)
  },
  has
  // ...
}
function size(target: any) {
  // 獲取原始數據
  target = toRaw(target)
  const proto = Reflect.getPrototypeOf(target)
  track(target, OperationTypes.ITERATE)
  return Reflect.get(proto, 'size', target)
}

function has(this: any, key: any): boolean {
  // 獲取原始數據
  const target = toRaw(this)
  key = toRaw(key)
  const proto: any = Reflect.getPrototypeOf(target)
  track(target, OperationTypes.HAS, key)
  return proto.has.call(target, key)
}
複製代碼

sizehas,都是「查」的邏輯。只是size是一個屬性,不是方法,因此須要以get size()的方式去劫持。而has是個方法,不須要專門綁定 this,二者內部邏輯也簡單,跟get基本一致。不過這裏有個關於 TypeScript 的小細節。has函數第一個入參是this,這個在 ts 裏是假的參數,真正調用這個函數的時候,是不須要傳遞的,因此依舊是這樣使用someMap.has(key)就好。

那除了這兩個查方法,還有迭代器相關的「查」方法。

插樁迭代器

關於迭代器,若是沒什麼瞭解,建議先閱讀相關文檔,好比MDN

// 迭代器相關的方法
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
  mutableInstrumentations[method] = createIterableMethod(method, false)
})
function createIterableMethod(method: string | symbol, isReadonly: boolean) {
  return function(this: any, ...args: any[]) {
    // 獲取原始數據
    const target = toRaw(this)
    // 獲取原型
    const proto: any = Reflect.getPrototypeOf(target)
    // 若是是entries方法,或者是map的迭代方法的話,isPair爲true
    // 這種狀況下,迭代器方法的返回的是一個[key, value]的結構
    const isPair =
      method === 'entries' ||
      (method === Symbol.iterator && target instanceof Map)
    // 調用原型鏈上的相應迭代器方法
    const innerIterator = proto[method].apply(target, args)
    // 獲取相應的轉成響應數據的方法
    const wrap = isReadonly ? toReadonly : toReactive
    // 收集依賴
    track(target, OperationTypes.ITERATE)
    // return a wrapped iterator which returns observed versions of the
    // values emitted from the real iterator
    // 給返回的innerIterator插樁,將其value值轉爲響應式數據
    return {
      // iterator protocol
      next() {
        const { value, done } = innerIterator.next()
        return done
          ? // 爲done的時候,value是最後一個值的next,是undefined,不必作響應式轉換了
            { value, done }
          : {
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
              done
            }
      },
      // iterable protocol
      [Symbol.iterator]() {
        return this
      }
    }
  }
}
複製代碼

這段邏輯其實還好,核心就是劫持迭代器方法,將每次next返回的 value 用reactive轉化。惟一會讓人不清楚的,實際上是對於Iterator以及Map|Set的不熟悉。若是確實不熟悉,建議仍是先看下它們的相關文檔。

迭代器相關的還有一個forEach方法。

function createForEach(isReadonly: boolean) {
  // 這個this,咱們已經知道了是假參數,也就是forEach的調用者
  return function forEach(this: any, callback: Function, thisArg?: any) {
    const observed = this
    const target = toRaw(observed)
    const proto: any = Reflect.getPrototypeOf(target)
    const wrap = isReadonly ? toReadonly : toReactive
    track(target, OperationTypes.ITERATE)
    // important: create sure the callback is
    // 1. invoked with the reactive map as `this` and 3rd arg
    // 2. the value received should be a corresponding reactive/readonly.
    // 將傳遞進來的callback方法插樁,讓傳入callback的數據,轉爲響應式數據
    function wrappedCallback(value: any, key: any) {
      // forEach使用的數據,轉爲響應式數據
      return callback.call(observed, wrap(value), wrap(key), observed)
    }
    return proto.forEach.call(target, wrappedCallback, thisArg)
  }
}
複製代碼

forEach的邏輯並不複雜,跟上面迭代器部分差很少,也是劫持了方法,將本來的傳參數據轉爲響應式數據後返回。

插樁寫操做

而後看寫操做。

function add(this: any, value: any) {
  // 獲取原始數據
  value = toRaw(value)
  const target = toRaw(this)
  // 獲取原型
  const proto: any = Reflect.getPrototypeOf(this)
  // 經過原型方法,判斷是否有這個key
  const hadKey = proto.has.call(target, value)
  // 經過原型方法,增長這個key
  const result = proto.add.call(target, value)
  // 本來沒有key的話,說明真的是新增,則觸發監聽響應邏輯
  if (!hadKey) {
    /* istanbul ignore else */
    if (__DEV__) {
      trigger(target, OperationTypes.ADD, value, { value })
    } else {
      trigger(target, OperationTypes.ADD, value)
    }
  }
  return result
}
複製代碼

咱們發現寫操做倒簡單多了,其實跟baseHandlers的邏輯是差很少的,只不過對於那些base數據,能夠經過Reflect方便的反射行爲,而在此處,須要手動獲取原型鏈並綁定this而已。查看setdeleteEntry的代碼,邏輯也差很少,就很少闡述了。

關於readonly相關的也很簡單了,我也不貼了代碼了,純粹增長文章字數。它就是將add|set|delete|clear這幾個寫方法再包一層,開發環境下拋個 warning。

到這裏,終於看完了collcetionsHandlers的所有邏輯了。

小總結

再總結一下它是如何劫持 collcetion 數據的。

  1. 因爲Set|Map等集合數據的底層設計問題,Proxy沒法直接劫持set或直接反射行爲。
  2. 劫持原始集合數據的get,對於它的原始方法或屬性,Reflect反射到插樁器上,不然反射原始對象。
  3. 插裝器上的方法,會先經過toRaw,獲取代理數據的原始數據,再獲取原始數據的原型方法,而後綁定this爲原始數據,調取相應方法。
  4. 對於getter|has這類查詢方法,插入收集依賴的邏輯,並將返回值轉爲響應式數據(has 返回 boolean 值故不須要轉換)。
  5. 對於迭代器相關的查詢方法,插入收集依賴邏輯,並將迭代過程的數據轉爲響應式數據。
  6. 對於寫操做相關方法,插入觸發監聽的邏輯。

其實原理仍是好理解的,只是寫起來比較麻煩。

總結

那到此爲止,終於把reactive的邏輯徹底理完了。閱讀本部分的代碼有點兒不容易,由於涉及的底層知識比較多,否則會到處懵逼,不過這也是一種學習,探索的過程也是挺有意思的。

在這過程當中,咱們發現,數組的劫持目前仍是存在一點點不足的,直接經過反射,會在一些狀況下重複觸發監聽函數。感受經過相似collection數據的處理方式能夠解決。可是這又增長了程序複雜度,並且也不知道會不會有一些其餘的坑。

另外,咱們發現閱讀reactivity相關的代碼時,ts 涉及的沒咱們想象中的多,內部不少狀況下是 any 的,但這是要辯證的看的。首先如小右所說,「這些數據是用戶數據,自己就是any的,勉強要聲明,沒有什麼意義」。並且那一路下來都是很是多的泛型加推導,成本很是高。反正我本身嘗試了下,是無能爲力的。另外當前代碼仍是非正式的階段,若是維護起來過於麻煩。那對於我這種 ts 半吊子的人,若是真的想再貢獻一點代碼,也是舉步維艱。

這篇文章有點兒繁雜,若是是慢慢看下來的,很是感謝你的閱讀~~

下篇是最後的effect相關的源碼解析,終於能解開最開始targetMap的謎團,看到tracktrigger的內部實現,湊上最後一塊拼圖了。

相關文章
相關標籤/搜索