Vue3.0 源碼分析(一):響應式模塊 reactivity

前言

學習 Vue3.0 源碼必須對如下知識有所瞭解:vue

  1. proxy reflect iterator
  2. map weakmap set weakset symbol

這些知識能夠看一下阮一峯老師的《ES6 入門教程》react

若是不會 ts,我以爲影響不大,瞭解一下泛型就能夠了。由於我就沒用過 TS,可是不影響看代碼。es6

閱讀源碼,建議先過一遍該模塊下的 API,瞭解一下有哪些功能。而後再看一遍相關的單元測試,單元測試通常會把全部的功能細節都測一邊。對源碼的功能有所瞭解後,再去閱讀源碼的細節,效果更好。數組

proxy 術語

const p = new Proxy(target, handler)
  • handler,包含捕捉器(trap)的佔位符對象,可譯爲處理器對象。
  • target,被 Proxy 代理的對象。

友情提醒

在閱讀源碼的過程當中,要時刻問本身三個問題:數據結構

  1. 這是什麼?
  2. 爲何要這樣?爲何不那樣?
  3. 有沒有更好的實現方式?

正所謂知其然,知其因此然。app

閱讀源碼除了要了解一個庫具備什麼特性,還要了解它爲何要這樣設計,而且要問本身能不能用更好的方式去實現它。
若是隻是單純的停留在「是什麼」這個階段,對你可能沒有什麼幫助。就像看流水帳似的,看完就忘,你得去思考,才能理解得更加深入。less

正文

reactivity 模塊是 Vue3.0 的響應式系統,它有如下幾個文件:函數

baseHandlers.ts
collectionHandlers.ts
computed.ts
effect.ts
index.ts
operations.ts
reactive.ts
ref.ts

接下來按重要程度順序來說解一下各個文件的 API 用法和實現。oop

reactive.ts 文件

在 Vue.2x 中,使用 Object.defineProperty() 對對象進行監聽。而在 Vue3.0 中,改用 Proxy 進行監聽。Proxy 比起 Object.defineProperty() 有以下優點:post

  1. 能夠監聽屬性的增刪操做。
  2. 能夠監聽數組某個索引值的變化以及數組長度的變化。

reactive()

reactive() 的做用主要是將目標轉化爲響應式的 proxy 實例。例如:

const obj = {
    count: 0
}

const proxy = reactive(obj)

若是是嵌套的對象,會繼續遞歸將子對象轉爲響應式對象。

reactive() 是向用戶暴露的 API,它真正執行的是 createReactiveObject() 函數:

// 根據 target 生成 proxy 實例
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.raw] &&
    !(isReadonly && target[ReactiveFlags.isReactive])
  ) {
    return target
  }
  // target already has corresponding Proxy
  if (
    hasOwn(target, isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive)
  ) {
    return isReadonly
      ? target[ReactiveFlags.readonly]
      : target[ReactiveFlags.reactive]
  }
  // only a whitelist of value types can be observed.
  if (!canObserve(target)) {
    return target
  }
 
  const observed = new Proxy(
    target,
    // 根據是否 Set, Map, WeakMap, WeakSet 來決定 proxy 的 handler 參數
    collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
  )
  // 在原始對象上定義一個屬性(只讀則爲 "__v_readonly",不然爲 "__v_reactive"),這個屬性的值就是根據原始對象生成的 proxy 實例。
  def(
    target,
    isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive,
    observed
  )
  
  return observed
}

這個函數的處理邏輯以下:

  1. 若是 target 不是一個對象,返回 target。
  2. 若是 target 已是 proxy 實例,返回 target。
  3. 若是 target 不是一個可觀察的對象,返回 target。
  4. 生成 proxy 實例,並在原始對象 target 上添加一個屬性(只讀則爲 __v_readonly,不然爲 __v_reactive),指向這個 proxy 實例,最後返回這個實例。添加這個屬性就是爲了在第 2 步作判斷用的,防止對同一對象重複監聽。

其中第 三、4 點須要單獨拎出來說一講。

什麼是可觀察的對象

const canObserve = (value: Target): boolean => {
  return (
    !value[ReactiveFlags.skip] &&
    isObservableType(toRawType(value)) &&
    !Object.isFrozen(value)
  )
}

canObserve() 函數就是用來判斷 value 是不是可觀察的對象,知足如下條件纔是可觀察的對象:

  1. ReactiveFlags.skip 的值不能爲 __v_skip__v_skip 是用來定義這個對象是否可跳過,即不監聽。
  2. target 的類型必須爲下列值之一 Object,Array,Map,Set,WeakMap,WeakSet 纔可被監聽。
  3. 不能是凍結的對象。

傳遞給 proxy 的處理器對象是什麼

根據上面的代碼能夠看出來,在生成 proxy 實例時,處理器對象是根據一個三元表達式產生的:

// collectionTypes 的值爲 Set, Map, WeakMap, WeakSet
collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers

這個三元表達式很是簡單,若是是普通的對象 ObjectArray,處理器對象就使用 baseHandlers;若是是 Set, Map, WeakMap, WeakSet 中的一個,就使用 collectionHandlers。

collectionHandlers 和 baseHandlers 是從 collectionHandlers.tsbaseHandlers.ts 處引入的,這裏先放一放,接下來再講。

有多少種 proxy 實例

createReactiveObject() 根據不一樣的參數,能夠建立多種不一樣的 proxy 實例:

  1. 徹底響應式的 proxy 實例,若是有嵌套對象,會遞歸調用 reactive()
  2. 只讀的 proxy 實例。
  3. 淺層響應的 proxy 實例,即一個對象只有第一層的屬性是響應式的。
  4. 只讀的淺層響應的 proxy 實例。

淺層響應的 proxy 實例是什麼?

之因此有淺層響應的 proxy 實例,是由於 proxy 只代理對象的第一層屬性,更深層的屬性是不會代理的。若是確實須要生成徹底響應式的 proxy 實例,就得遞歸調用 reactive()。不過這個過程是內部自動執行的,用戶感知不到。

其餘一些函數介紹

// 判斷 value 是不是響應式的
export function isReactive(value: unknown): boolean {
  if (isReadonly(value)) {
    return isReactive((value as Target)[ReactiveFlags.raw])
  }
  return !!(value && (value as Target)[ReactiveFlags.isReactive])
}
// 判斷 value 是不是隻讀的
export function isReadonly(value: unknown): boolean {
  return !!(value && (value as Target)[ReactiveFlags.isReadonly])
}
// 判斷 value 是不是 proxy 實例
export function isProxy(value: unknown): boolean {
  return isReactive(value) || isReadonly(value)
}

// 將響應式數據轉爲原始數據,若是不是響應數據,則返回源數據
export function toRaw<T>(observed: T): T {
  return (
    (observed && toRaw((observed as Target)[ReactiveFlags.raw])) || observed
  )
}

// 給 value 設置 skip 屬性,跳過代理,讓數據不可被代理
export function markRaw<T extends object>(value: T): T {
  def(value, ReactiveFlags.skip, true)
  return value
}

baseHandlers.ts 文件

baseHandlers.ts 文件中針對 4 種 proxy 實例定義了不對的處理器。
因爲它們之間差異不大,因此在這隻講解徹底響應式的處理器對象:

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

處理器對五種操做進行了攔截,分別是:

  1. get 屬性讀取
  2. set 屬性設置
  3. deleteProperty 刪除屬性
  4. has 是否擁有某個屬性
  5. ownKeys

其中 ownKeys 可攔截如下操做:

  1. Object.getOwnPropertyNames()
  2. Object.getOwnPropertySymbols()
  3. Object.keys()
  4. Reflect.ownKeys()

其中 get、has、ownKeys 操做會收集依賴,set、deleteProperty 操做會觸發依賴。

get

get 屬性的處理器是用 createGetter() 函數建立的:

// /*#__PURE__*/ 標識此爲純函數 不會有反作用 方便作 tree-shaking
const get = /*#__PURE__*/ createGetter()

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // target 是不是響應式對象
    if (key === ReactiveFlags.isReactive) {
      return !isReadonly
      // target 是不是隻讀對象
    } else if (key === ReactiveFlags.isReadonly) {
      return isReadonly
    } else if (
      // 若是訪問的 key 是 __v_raw,而且 receiver == target.__v_readonly || receiver == target.__v_reactive
      // 則直接返回 target
      key === ReactiveFlags.raw &&
      receiver ===
        (isReadonly
          ? (target as any).__v_readonly
          : (target as any).__v_reactive)
    ) {
      return target
    }

    const targetIsArray = isArray(target)
    // 若是 target 是數組而且 key 屬於三個方法之一 ['includes', 'indexOf', 'lastIndexOf'],即觸發了這三個操做之一
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // 無論Proxy怎麼修改默認行爲,你總能夠在Reflect上獲取默認行爲。
    // 若是不用 Reflect 來獲取,在監聽數組時能夠會有某些地方會出錯
    // 具體請看文章《Vue3 中的數據偵測》——https://juejin.im/post/5d99be7c6fb9a04e1e7baa34#heading-10
    const res = Reflect.get(target, key, receiver)

    // 若是 key 是 symbol 而且屬於 symbol 的內置方法之一,或者訪問的是原型對象,直接返回結果,不收集依賴。
    if ((isSymbol(key) && builtInSymbols.has(key)) || key === '__proto__') {
      return res
    }

    // 只讀對象不收集依賴
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    
    // 淺層響應當即返回,不遞歸調用 reactive()
    if (shallow) {
      return res
    }

    // 若是是 ref 對象,則返回真正的值,即 ref.value,數組除外。
    if (isRef(res)) {
      // ref unwrapping, only for Objects, not for Arrays.
      return targetIsArray ? res : res.value
    }

    if (isObject(res)) {
      // 因爲 proxy 只能代理一層,因此 target[key] 的值若是是對象,就繼續對其進行代理
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

這個函數的處理邏輯看代碼註釋應該就能明白,其中有幾個點須要單獨說一下:

  1. Reflect.get()
  2. 數組的處理
  3. builtInSymbols.has(key) 爲 true 或原型對象不收集依賴

Reflect.get()

Reflect.get() 方法與從對象 (target[key]) 中讀取屬性相似,但它是經過一個函數執行來操做的。

爲何直接用 target[key] 就能獲得值,卻還要用 Reflect.get(target, key, receiver) 來多倒一手呢?

先來看個簡單的示例:

const p = new Proxy([1, 2, 3], {
    get(target, key, receiver) {
        return target[key]
    },
    set(target, key, value, receiver) {
        target[key] = value
    }
})

p.push(100)

運行這段代碼會報錯:

Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'

但作一些小改動就可以正常運行:

const p = new Proxy([1, 2, 3], {
    get(target, key, receiver) {
        return target[key]
    },
    set(target, key, value, receiver) {
        target[key] = value
        return true // 新增一行 return true
    }
})

p.push(100)

這段代碼能夠正常運行。爲何呢?

區別在於新的這段代碼在 set() 方法上多了一個 return true。我在 MDN 上查找到的解釋是這樣的:

set() 方法應當返回一個布爾值。

  • 返回 true 表明屬性設置成功。
  • 在嚴格模式下,若是 set() 方法返回 false,那麼會拋出一個 TypeError 異常。

這時我又試了一下直接執行 p[3] = 100,發現能正常運行,只有執行 push 方法才報錯。到這一步,我心中已經有答案了。爲了驗證個人猜測,我在代碼上加了 console.log(),把代碼執行過程的一些屬性打印出來。

const p = new Proxy([1, 2, 3], {
    get(target, key, receiver) {
        console.log('get: ', key)
        return target[key]
    },
    set(target, key, value, receiver) {
        console.log('set: ', key, value)
        target[key] = value
        return true
    }
})

p.push(100)

// get:  push
// get:  length
// set:  3 100
// set:  length 4

從上面的代碼能夠發現執行 push 操做時,還會訪問 length 屬性。推測執行過程以下:根據 length 的值,得出最後的索引,再設置新的置,最後再改變 length

結合 MDN 的解釋,個人推測是數組的原生方法應該是運行在嚴格模式下的(若是有網友知道真相,請在評論區留言)。由於在 JS 中不少代碼在非嚴格模式和嚴格模式下都能正常運行,只是嚴格模式會給你報個錯。就跟此次狀況同樣,最後設置 length 屬性的時候報錯,但結果仍是正常的。若是不想報錯,就得每次都返回 true

而後再看一下 Reflect.set() 的返回值說明:

返回一個 Boolean 值代表是否成功設置屬性。

因此上面代碼能夠改爲這樣:

const p = new Proxy([1, 2, 3], {
    get(target, key, receiver) {
        console.log('get: ', key)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log('set: ', key, value)
        return Reflect.set(target, key, value, receiver)
    }
})

p.push(100)

另外,無論 Proxy 怎麼修改默認行爲,你總能夠在 Reflect 上獲取默認行爲。

經過上面的示例,不難理解爲何要經過 Reflect.set() 來代替 Proxy 完成默認操做了。同理,Reflect.get() 也同樣。

數組的處理

// 若是 target 是數組而且 key 屬於三個方法之一 ['includes', 'indexOf', 'lastIndexOf'],即觸發了這三個操做之一
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
  return Reflect.get(arrayInstrumentations, key, receiver)
}

在執行數組的 includes, indexOf, lastIndexOf 方法時,會把目標對象轉爲 arrayInstrumentations 再執行。

const arrayInstrumentations: Record<string, Function> = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
  arrayInstrumentations[key] = function(...args: any[]): any {
    // 若是 target 對象中指定了 getter,receiver 則爲 getter 調用時的 this 值。
    // 因此這裏的 this 指向 receiver,即 proxy 實例,toRaw 爲了取得原始數據
    const arr = toRaw(this) as any
    // 對數組的每一個值進行 track 操做,收集依賴
    for (let i = 0, l = (this as any).length; i < l; i++) {
      track(arr, TrackOpTypes.GET, i + '')
    }
    // we run the method using the original args first (which may be reactive)
    // 參數有多是響應式的,函數執行後返回值爲 -1 或 false,那就用參數的原始值再試一遍
    const res = arr[key](...args)
    if (res === -1 || res === false) {
      // if that didn't work, run it again using raw values.
      return arr[key](...args.map(toRaw))
    } else {
      return res
    }
  }
})

從上述代碼能夠看出,Vue3.0 對 includes, indexOf, lastIndexOf 進行了封裝,除了返回原有方法的結果外,還會對數組的每一個值進行依賴收集。

builtInSymbols.has(key) 爲 true 或原型對象不收集依賴

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

p.toString() // get:  toString
             // get:  Symbol(Symbol.toStringTag)
p.__proto__  // get:  __proto__

p.toString() 的執行結果來看,它會觸發兩次 get,一次是咱們想要的,一次是咱們不想要的(我還沒搞明白爲何會有 Symbol(Symbol.toStringTag),若是有網友知道,請在評論區留言)。因此就有了這個判斷: builtInSymbols.has(key)true 就直接返回,防止重複收集依賴。

再看 p.__proto__ 的執行結果,也觸發了一次 get 操做。通常來講,沒有場景須要單獨訪問原型,訪問原型都是爲了訪問原型上的方法,例如 p.__proto__.toString() 這樣使用,因此 key 爲 __proto__ 的時候也要跳過,不收集依賴。

set

const set = /*#__PURE__*/ createSetter()

// 參考文檔《Vue3 中的數據偵測》——https://juejin.im/post/5d99be7c6fb9a04e1e7baa34#heading-10
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      // 若是原來的值是 ref,但新的值不是,將新的值賦給 ref.value 便可。
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    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)) {
      if (!hadKey) {
        // 若是 target 沒有 key,就表明是新增操做,須要觸發依賴
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 若是新舊值不相等,才觸發依賴
        // 何時會有新舊值相等的狀況?例如監聽一個數組,執行 push 操做,會觸發屢次 setter
        // 第一次 setter 是新加的值 第二次是因爲新加的值致使 length 改變
        // 但因爲 length 也是自身屬性,因此 value === oldValue
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

set() 的函數處理邏輯反而沒那麼難,看註釋便可。track()trigger() 將放在下面和 effect.ts 文件一塊兒講解。

deleteProperty、has、ownKeys

function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  // 若是刪除結果爲 true 而且 target 擁有這個 key 就觸發依賴
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  track(target, TrackOpTypes.HAS, key)
  return result
}

function ownKeys(target: object): (string | number | symbol)[] {
  track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.ownKeys(target)
}

這三個函數比較簡單,看代碼便可。

effect.ts 文件

等把 effect.ts 文件講解完,響應式模塊基本上差很少結束了。

effect()

effect() 主要和響應式的對象結合使用。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // 若是已是 effect 函數,取得原來的 fn
  if (isEffect(fn)) {
    fn = fn.raw
  }
  
  const effect = createReactiveEffect(fn, options)
  // 若是 lazy 爲 false,立刻執行一次
  // 計算屬性的 lazy 爲 true
  if (!options.lazy) {
    effect()
  }
  
  return effect
}

真正建立 effect 的是 createReactiveEffect() 函數。

let uid = 0

function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  // reactiveEffect() 返回一個新的 effect,這個新的 effect 執行後
  // 會將本身設爲 activeEffect,而後再執行 fn 函數,若是在 fn 函數裏對響應式屬性進行讀取
  // 會觸發響應式屬性 get 操做,從而收集依賴,而收集的這個依賴函數就是 activeEffect
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn(...args)
    }
    // 爲了不遞歸循環,因此要檢測一下
    if (!effectStack.includes(effect)) {
      // 清空依賴
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn(...args)
      } finally {
        // track 將依賴函數 activeEffect 添加到對應的 dep 中,而後在 finally 中將 activeEffect
        // 重置爲上一個 effect 的值
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
        
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect._isEffect = true
  effect.active = true // 用於判斷當前 effect 是否激活,有一個 stop() 來將它設爲 false
  effect.raw = fn
  effect.deps = []
  effect.options = options
  
  return effect
}

其中 cleanup(effect) 的做用是讓 effect 關聯下的全部 dep 實例清空 effect,即清除這個依賴函數。

function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

從代碼中能夠看出來,真正的依賴函數是 activeEffect。執行 track() 收集的依賴就是 activeEffect。
趁熱打鐵,如今咱們再來看一下 track()trigger() 函數。

track()

// 依賴收集
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // activeEffect 爲空,表明沒有依賴,直接返回
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // targetMap 依賴管理中心,用於收集依賴和觸發依賴
  let depsMap = targetMap.get(target)
  // targetMap 爲每一個 target 創建一個 map
  // 每一個 target 的 key 對應着一個 dep
  // 而後用 dep 來收集依賴函數,當監聽的 key 值發生變化時,觸發 dep 中的依賴函數
  // 相似於這樣
  // targetMap(weakmap) = {
  //     target1(map): {
  //       key1(dep): (fn1,fn2,fn3...)
  //       key2(dep): (fn1,fn2,fn3...)
  //     },
  //     target2(map): {
  //       key1(dep): (fn1,fn2,fn3...)
  //       key2(dep): (fn1,fn2,fn3...)
  //     },
  // }
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    // 開發環境下會觸發 onTrack 事件
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

targetMap 是一個 WeakMap 實例。

WeakMap 對象是一組鍵/值對的集合,其中的鍵是弱引用的。其鍵必須是對象,而值能夠是任意的。

弱引用是什麼意思呢?

let obj = { a: 1 }
const map = new WeakMap()
map.set(obj, '測試')
obj = null

當 obj 置爲空後,對於 { a: 1 } 的引用已經爲零了,下一次垃圾回收時就會把 weakmap 中的對象回收。

但若是把 weakmap 換成 map 數據結構,即便把 obj 置空,{ a: 1 } 依然不會被回收,由於 map 數據結構是強引用,它如今還被 map 引用着。

trigger()

// 觸發依賴
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  
  const depsMap = targetMap.get(target)
  // 若是沒有收集過依賴,直接返回
  if (!depsMap) {
    // never been tracked
    return
  }
  
  // 對收集的依賴進行分類,分爲普通的依賴或計算屬性依賴
  // effects 收集的是普通的依賴 computedRunners 收集的是計算屬性的依賴
  // 兩個隊列都是 set 結構,爲了不重複收集依賴
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        // effect !== activeEffect 避免重複收集依賴
        if (effect !== activeEffect || !shouldTrack) {
          // 計算屬性
          if (effect.options.computed) {
            computedRunners.add(effect)
          } else {
            effects.add(effect)
          }
        } else {
          // the effect mutated its own dependency during its execution.
          // this can be caused by operations like foo.value++
          // do not trigger or we end in an infinite loop
        }
      })
    }
  }

  // 在值被清空前,往相應的隊列添加 target 全部的依賴
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) { // 當數組的 length 屬性變化時觸發
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    // 若是不符合以上兩個 if 條件,而且 key !== undefined,往相應的隊列添加依賴
    if (key !== void 0) {
      add(depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE | Map.SET
    const isAddOrDelete =
      type === TriggerOpTypes.ADD ||
      (type === TriggerOpTypes.DELETE && !isArray(target))

    if (
      isAddOrDelete ||
      (type === TriggerOpTypes.SET && target instanceof Map)
    ) {
      add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
    }
    
    if (isAddOrDelete && target instanceof Map) {
      add(depsMap.get(MAP_KEY_ITERATE_KEY))
    }
  }

  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      // 若是 scheduler 存在則調用 scheduler,計算屬性擁有 scheduler
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  computedRunners.forEach(run)
  // 觸發依賴函數
  effects.forEach(run)
}

對依賴函數進行分類後,須要先運行計算屬性的依賴,由於其餘普通的依賴函數可能包含了計算屬性。先執行計算屬性的依賴能保證普通依賴執行時能獲得最新的計算屬性的值。

track() 和 trigger() 中的 type 有什麼用?

這個 type 取值範圍就定義在 operations.ts 文件中:

// track 的類型
export const enum TrackOpTypes {
  GET = 'get', // get 操做
  HAS = 'has', // has 操做
  ITERATE = 'iterate' // ownKeys 操做
}

// trigger 的類型
export const enum TriggerOpTypes {
  SET = 'set', // 設置操做,將舊值設置爲新值
  ADD = 'add', // 新增操做,添加一個新的值 例如給對象新增一個值 數組的 push 操做
  DELETE = 'delete', // 刪除操做 例如對象的 delete 操做,數組的 pop 操做
  CLEAR = 'clear' // 用於 Map 和 Set 的 clear 操做。
}

type 主要用於標識 track()trigger() 的類型。

trigger() 中的連續判斷代碼

if (key !== void 0) {
  add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
const isAddOrDelete =
  type === TriggerOpTypes.ADD ||
  (type === TriggerOpTypes.DELETE && !isArray(target))

if (
  isAddOrDelete ||
  (type === TriggerOpTypes.SET && target instanceof Map)
) {
  add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}

if (isAddOrDelete && target instanceof Map) {
  add(depsMap.get(MAP_KEY_ITERATE_KEY))
}

trigger() 中有這麼一段連續判斷的代碼,它們做用是什麼呢?其實它們是用於判斷數組/集合這種數據結構比較特別的操做。
看個示例:

let dummy
const counter = reactive([])
effect(() => (dummy = counter.join()))
counter.push(1)

effect(() => (dummy = counter.join())) 生成一個依賴,而且自執行一次。
在執行函數裏的代碼 counter.join() 時,會訪問數組的多個屬性,分別是 joinlength,同時觸發 track() 收集依賴。也就是說,數組的 join length 屬性都收集了一個依賴。

當執行 counter.push(1) 這段代碼時,其實是將數組的索引 0 對應的值設爲 1。這一點,能夠經過打 debugger 從上下文環境看出來,其中 key 爲 0,即數組的索引,值爲 1。

設置值後,因爲是新增操做,執行 trigger(target, TriggerOpTypes.ADD, key, value)。但由上文可知,只有數組的 key 爲 join length 時,纔有依賴,key 爲 0 是沒有依賴的。

從上面兩個圖能夠看出來,只有 join length 屬性纔有對應的依賴。

這個時候,trigger() 的一連串 if 語句就起做用了,其中有一個 if 語句是這樣的:

if (
  isAddOrDelete ||
  (type === TriggerOpTypes.SET && target instanceof Map)
) {
  add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}

若是 target 是一個數組,就添加 length 屬性對應的依賴到隊列中。也就是說 key 爲 0 的狀況下使用 length 對應的依賴。

另外,還有一個巧妙的地方。待執行依賴的隊列是一個 set 數據結構。若是 key 爲 0 有對應的依賴,同時 length 也有對應的依賴,就會添加兩次依賴,但因爲隊列是 set,具備自動去重的效果,避免了重複執行。

示例

僅看代碼和文字,是很難理解響應式數據和 track() trigger() 是怎麼配合的。因此咱們要配合示例來理解:

let dummy
const counter = reactive({ num: 0 })
effect(() => (dummy = counter.num))

console.log(dummy == 0)
counter.num = 7
console.log(dummy == 7)

上述代碼執行過程以下:

  1. { num: 0 } 進行監聽,返回一個 proxy 實例,即 counter。
  2. effect(fn) 建立一個依賴,而且在建立時會執行一次 fn
  3. fn() 讀取 num 的值,並賦值給 dummy。
  4. 讀取屬性這個操做會觸發 proxy 的屬性讀取攔截操做,在攔截操做裏會去收集依賴,這個依賴是步驟 2 產生的。
  5. counter.num = 7 這個操做會觸發 proxy 的屬性設置攔截操做,在這個攔截操做裏,除了把新的值返回,還會觸發剛纔收集的依賴。在這個依賴裏把 counter.num 賦值給 dummy(num 的值已經變爲 7)。

用圖來表示,大概這樣的:

collectionHandlers.ts 文件

collectionHandlers.ts 文件包含了 Map WeakMap Set WeakSet 的處理器對象,分別對應徹底響應式的 proxy 實例、淺層響應的 proxy 實例、只讀 proxy 實例。這裏只講解對應徹底響應式的 proxy 實例的處理器對象:

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(false, false)
}

爲何只監聽 get 操做,set has 等操做呢?不着急,先看一個示例:

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

p.set('ab', 100) // Uncaught TypeError: Method Map.prototype.set called on incompatible receiver [object Object]

運行上面的代碼會報錯。其實這和 Map Set 的內部實現有關,必須經過 this 才能訪問它們的數據。可是經過 Reflect 反射的時候,target 內部的 this 實際上是指向 proxy 實例的,因此就不難理解爲何會報錯了。

那怎麼解決這個問題?經過源碼能夠發現,在 Vue3.0 中是經過代理的方式來實現對 Map Set 等數據結構監聽的:

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? shallowInstrumentations
    : isReadonly
      ? readonlyInstrumentations
      : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    // 這三個 if 判斷和 baseHandlers 的處理方式同樣
    if (key === ReactiveFlags.isReactive) {
      return !isReadonly
    } else if (key === ReactiveFlags.isReadonly) {
      return isReadonly
    } else if (key === ReactiveFlags.raw) {
      return target
    }

    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

把最後一行代碼簡化一下:

target = hasOwn(instrumentations, key) && key in target? instrumentations : target
return Reflect.get(target, key, receiver);

其中 instrumentations 的內容是:

const mutableInstrumentations: Record<string, Function> = {
  get(this: MapTypes, key: unknown) {
    return get(this, key, toReactive)
  },
  get size() {
    return size((this as unknown) as IterableCollections)
  },
  has,
  add,
  set,
  delete: deleteEntry,
  clear,
  forEach: createForEach(false, false)
}

從代碼能夠看到,原來真正的處理器對象是 mutableInstrumentations。如今再看一個示例:

const proxy = reactive(new Map())
proxy.set('key', 100)

生成 proxy 實例後,執行 proxy.set('key', 100)proxy.set 這個操做會觸發 proxy 的屬性讀取攔截操做。

打斷點能夠看到,此時的 key 爲 set。攔截了 set 操做後,調用 Reflect.get(target, key, receiver),這個時候的 target 已經不是原來的 target 了,而是 mutableInstrumentations 對象。也就是說,最終執行的是 mutableInstrumentations.set()

接下來再看看 mutableInstrumentations 的各個處理器邏輯。

get

// 若是 value 是對象,則返回一個響應式對象(`reactive(value)`),不然直接返回 value。
const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value
  
get(this: MapTypes, key: unknown) {
    // this 指向 proxy
    return get(this, key, toReactive)
}
  
function get(
  target: MapTypes,
  key: unknown,
  wrap: typeof toReactive | typeof toReadonly | typeof toShallow
) {
  target = toRaw(target)
  const rawKey = toRaw(key)
  // 若是 key 是響應式的,額外收集一次依賴
  if (key !== rawKey) {
    track(target, TrackOpTypes.GET, key)
  }
  track(target, TrackOpTypes.GET, rawKey)
  // 使用 target 原型上的方法
  const { has, get } = getProto(target)
  // 原始 key 和響應式的 key 都試一遍
  if (has.call(target, key)) {
    // 讀取的值要使用包裝函數處理一下
    return wrap(get.call(target, key))
  } else if (has.call(target, rawKey)) {
    return wrap(get.call(target, rawKey))
  }
}

get 的處理邏輯很簡單,攔截 get 以後,調用 get(this, key, toReactive)

set

function set(this: MapTypes, key: unknown, value: unknown) {
  value = toRaw(value)
  // 取得原始數據
  const target = toRaw(this)
  // 使用 target 原型上的方法
  const { has, get, set } = getProto(target)

  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    checkIdentityKeys(target, has, key)
  }

  const oldValue = get.call(target, key)
  const result = set.call(target, key, value)
  // 防止重複觸發依賴,若是 key 已存在就不觸發依賴
  if (!hadKey) {
    trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue)) {
    // 若是新舊值相等,也不會觸發依賴
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  return result
}

set 的處理邏輯也較爲簡單,配合註釋一目瞭然。

還有剩下的 has add delete 等方法就不講解了,代碼行數比較少,邏輯也很簡單,建議自行閱讀。

ref.ts 文件

const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val
  
export function ref(value?: unknown) {
  return createRef(value)
}
  
function createRef(rawValue: unknown, shallow = false) {
  // 若是已是 ref 對象了,直接返回原值
  if (isRef(rawValue)) {
    return rawValue
  }
  
  // 若是不是淺層響應而且 rawValue 是個對象,調用 reactive(rawValue)
  let value = shallow ? rawValue : convert(rawValue)
  
  const r = {
    __v_isRef: true, // 用於標識這是一個 ref 對象,防止重複監聽 ref 對象
    get value() {
      // 讀取值時收集依賴
      track(r, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newVal) {
      if (hasChanged(toRaw(newVal), rawValue)) {
        rawValue = newVal
        value = shallow ? newVal : convert(newVal)
        // 設置值時觸發依賴
        trigger(
          r,
          TriggerOpTypes.SET,
          'value',
          __DEV__ ? { newValue: newVal } : void 0
        )
      }
    }
  }
  
  return r
}

在 Vue2.x 中,基本數值類型是不能監聽的。但在 Vue3.0 中經過 ref() 能夠實現這一效果。

const r = ref(0)
effect(() => console.log(r.value)) // 打印 0
r.value++ // 打印 1

ref() 會把 0 轉成一個 ref 對象。若是給 ref(value) 傳的值是個對象,在函數內部會調用 reactive(value) 將其轉爲 proxy 實例。

computed.ts 文件

export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
  // 若是 getterOrOptions 是個函數,則是不可被配置的,setter 設爲空函數
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    // 若是是個對象,則可讀可寫
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  // dirty 用於判斷計算屬性依賴的響應式屬性有沒有被改變
  let dirty = true
  let value: T
  let computed: ComputedRef<T>

  const runner = effect(getter, {
    lazy: true, // lazy 爲 true,生成的 effect 不會立刻執行
    // mark effect as computed so that it gets priority during trigger
    computed: true,
    scheduler: () => { // 調度器
      // trigger 時,計算屬性執行的是 effect.options.scheduler(effect) 而不是 effect()
      if (!dirty) {
        dirty = true
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
  
  computed = {
    __v_isRef: true,
    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  return computed
}

下面經過一個示例,來說解一下 computed 是怎麼運做的:

const value = reactive({})
const cValue = computed(() => value.foo)
console.log(cValue.value === undefined)
value.foo = 1
console.log(cValue.value === 1)
  1. 生成一個 proxy 實例 value。
  2. computed() 生成計算屬性對象,當對 cValue 進行取值時(cValue.value),根據 dirty 判斷是否須要運行 effect 函數進行取值,若是 dirty 爲 false,直接把值返回。
  3. 在 effect 函數裏將 effect 設爲 activeEffect,並運行 getter(() => value.foo) 取值。在取值過程當中,讀取 foo 的值(value.foo)。
  4. 這會觸發 get 屬性讀取攔截操做,進而觸發 track 收集依賴,而收集的依賴函數就是第 3 步產生的 activeEffect。
  5. 當響應式屬性進行從新賦值時(value.foo = 1),就會 trigger 這個 activeEffect 函數。
  6. 而後調用 scheduler() 將 dirty 設爲 true,這樣 computed 下次求值時會從新執行 effect 函數進行取值。

index.ts 文件

index.ts 文件向外導出 reactivity 模塊的 API。

參考資料

相關文章
相關標籤/搜索