首頁 > 軟體

Vue3 原始碼解讀之副作用函數與依賴收集

2022-08-11 18:01:35

版本:3.2.31

副作用函數

副作用函數是指會產生副作用的函數,如下面的程式碼所示:

function effect(){
  document.body.innerText = 'hello vue3'
}

當 effect 函數執行時,它會設定 body 的文字內容,但除了 effect 函數之外的任何函數都可以讀取或設定 body 的文字內容。也就是說,effect 函數的執行會直接或間接影響其他函數的執行,這時我們說 effect 函數產生了副作用。副作用很容易產生,例如一個函數修改了全域性變數,這其實也是一個副作用。

// 全域性變數
let val = 1
function effect() {
  val = 2 // 修改全域性變數,產生副作用
}

副作用函數的全域性變數

在副作用模組中,定義了幾個全域性的變數,提前認識這些變數有助與我們瞭解副作用函數的生成以及呼叫的過程。

// packages/reactivity/src/effect.ts
export type Dep = Set<ReactiveEffect> & TrackedMarkers
type KeyToDepMap = Map<any, Dep>
// WeakMap 集合儲存副作用函數
const targetMap = new WeakMap<any, KeyToDepMap>()

// 用一個全域性變數儲存當前啟用的 effect 函數
export let activeEffect: ReactiveEffect | undefined

// 標識是否開啟了依賴收集
export let shouldTrack = true
const trackStack: boolean[] = []

targetMap

targetMap 是一個 WeakMap 型別的集合,用來儲存副作用函數,從型別定義可以看出 targetMap的資料結構方式:

  • WeakMap 由 target --> Map 構成
  • Map 由 key --> Set 構成

其中 WeakMap 的鍵是原始物件 target,WeakMap 的值是一個 Map 範例,Map 的鍵是原始物件 target 的 key,Map 的值是一個由副作用函陣列成的 Set。它們的關係如下:

targetMap 為什麼使用 WeakMap

我們來看下面的程式碼:

const map = new Map();
const weakMap = new WeakMap();

(function() {
  const foo = {foo: 1};
  const bar = {bar: 2};
  
  map.set(foo, 1); // foo 物件是 map 的key
  weakMap.set(bar, 2); // bar 物件是 weakMap 的 key
})

在上面的程式碼中,定義了 map 和 weakMap 常數,分別對應 Map 和 WeakMap 的範例。在立即執行的函數表示式內部定義了兩個物件:foo 和 bar,這兩個物件分別作為 map 和 weakMap 的key。

當函數表示式執行完畢後,對於物件 foo 來說,它仍然作為 map 的 key 被參照著,因此垃圾回收器不會把它從記憶體中移除,我們仍然可以通過 map.keys 列印出物件 foo 。

對於物件 bar 來說,由於 WeakMap 的 key 是弱參照,它不影響垃圾收集器的工作,所以一旦表示式執行完畢,垃圾回收器就會把物件 bar 從記憶體中移除,並且我們無法獲取 weakMap 的 key 值,也就無法通過 weakMap 取得物件 bar 。

簡單地說,WeakMap 對 key 是弱參照,不影響垃圾回收器的工作**。根據這個特性可知,一旦 key 被垃圾回收器回收,那麼對應的鍵和值就存取不到了。所以 WeakMap 經常用於儲存那些只有當 key 所參照的物件存在時 (沒有被回收) 才有價值的資訊**。

例如在上面的場景中,如果 target 物件沒有任何參照了,說明使用者側不再需要它了,這時垃圾回收器會完成回收任務。但如果使用 Map 來代替 WeakMap,那麼即使使用者側的程式碼對 target 沒有任何參照,這個 target 也不會被回收,最終可能導致記憶體溢位。

activeEffect

activeEffect 變數用來維護當前正在執行的副作用

shouldTrack

shouldTrack 變數用來標識是否開啟依賴蒐集,只有 shouldTrack 的值為 true 時,才進行依賴收集,即將副作用函數新增到依賴集合中。

副作用的實現

effect 函數

effect API 用來建立一個副作用函數,接受兩個引數,分別是使用者自定義的fn函數和options 選項。原始碼如下所示:

// packages/reactivity/src/effect.ts

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  // 當傳入的 fn 中存在 effect 副作用時,將這個副作用的原始函數賦值給 fn
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }

  // 建立一個副作用 
  const _effect = new ReactiveEffect(fn)

  if (options) {
    extend(_effect, options)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }

//   如果不是延遲執行的,則立即執行一次副作用函數
  if (!options || !options.lazy) {
    _effect.run()
  }
  // 通過 bind 函數返回一個新的副作用函數   
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  // 將副作用新增到新的副作用函數上   
  runner.effect = _effect
  // 返回這個新的副作用函數   
  return runner
}

由上面的程式碼可以知道,當傳入的引數 fn 中存在 effect 副作用時,將這個副作用的原始函數賦值給 fn。然後呼叫 ReactiveEffect 類建立一個封裝後的副作用函數。

在有些場景下,我們不希望 effect 立即執行,而是希望它在需要的時候才執行,我們可以通過在 options 中新增 lazy 屬性來達到目的。在 effect 函數原始碼中,判斷 options.lazy 選項的值,當值為true 時,則不立即執行副作用函數,從而實現懶執行的 effect。

接著通過 bind 函數返回一個新的副作用函數runner,這個新函數的this被指定為 _effect,並將 _effect 新增到這個新副作用函數的 effect 屬性上,最後返回這個新副作用函數。

由於 effect API 返回的是封裝後的副作用函數,原始的副作用函數儲存在封裝後的副作用函數的effect屬性上,因此如果想要獲取使用者傳入的副作用函數,需要通過 fn.effect.fn 來獲取。

在 effect 函數中呼叫了 ReactiveEffect 類建立副作用,接下來看看 ReactiveEffect 類的實現。

ReactiveEffect 類

// packages/reactivity/src/effect.ts

export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined

  /**
   * Can be attached after creation
   * @internal
   */
  computed?: ComputedRefImpl<T>
  /**
   * @internal
   */
  allowRecurse?: boolean

  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope)
  }
  
  run() {
    // 如果 effect 已停用,返回原始副作用函數執行後的結果
    if (!this.active) {
      return this.fn()
    }
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    try {
      // 建立一個新的副作用前將當前正在執行的副作用儲存到新建的副作用的 parent 屬性上,解決巢狀effect 的情況
      this.parent = activeEffect
      // 將建立的副作用設定為當前正則正在執行的副作用   
      activeEffect = this
      // 將 shouldTrack 設定為 true,表示開啟依賴收集
      shouldTrack = true

      trackOpBit = 1 << ++effectTrackDepth

      if (effectTrackDepth <= maxMarkerBits) {
        // 初始化依賴
        initDepMarkers(this)
      } else {
        // 清除依賴
        cleanupEffect(this)
      }
    //   返回原始副作用函數執行後的結果
      return this.fn()
    } finally {
      if (effectTrackDepth <= maxMarkerBits) {
        finalizeDepMarkers(this)
      }

      trackOpBit = 1 << --effectTrackDepth

      // 重置當前正在執行的副作用   
      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      this.parent = undefined
    }
  }
  // 停止(清除) effect
  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

在 ReactiveEffect 類中,定義了一個 run 方法,這個 run 方法就是建立副作用時實際執行方法。每次派發更新時,都會執行這個run方法,從而更新值。

全域性變數 activeEffect 用來維護當前正在執行的副作用,當存在巢狀渲染元件的時候,依賴收集後,副作用函數會被覆蓋,即 activeEffect 儲存的副作用函數在巢狀 effect 的時候會被內層的副作用函數覆蓋。為了解決這個問題,在 run 方法中,將當前正在執行的副作用activeEffect儲存到新建的副作用的 parent 屬性上,然後再將新建的副作用設定為當前正在執行的副作用。在新建的副作用執行完畢後,再將儲存到 parent 屬性的副作用重新設定為當前正在執行的副作用。

在 ReactiveEffect 類中,還定義了一個 stop 方法,該方法用來停止並清除當前正在執行的副作用。

track 收集依賴

當使用代理物件存取物件的屬性時,就會觸發代理物件的 get 攔截函數執行,如下面的程式碼所示:

const obj = { foo: 1 }

const p = new Proxy(obj, {
  get(target, key, receiver) {
    track(target, key)
    return Reflect.get(target, key, receiver)
  }
}) 

p.foo

在上面的程式碼中,通過代理物件p 存取 foo 屬性,便會觸發 get 攔截函數的執行,此時就在 get 攔截函數中呼叫 track 函數進行依賴收集。原始碼中 get 攔截函數的解析可閱讀《Vue3 原始碼解讀之非原始值的響應式原理》一文中的「存取屬性的攔截」小節。

下面,我們來看看 track 函數的實現。

track 函數

// packages/reactivity/src/effect.ts

// 收集依賴
export function track(target: object, type: TrackOpTypes, key: unknown) {
    // 如果開啟了依賴收集並且有正在執行的副作用,則收集依賴
  if (shouldTrack && activeEffect) {
    // 在 targetMap 中獲取對應的 target 的依賴集合
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      // 如果 target 不在 targetMap 中,則加入,並初始化 value 為 new Map()
      targetMap.set(target, (depsMap = new Map()))
    }
    // 從依賴集合中獲取對應的 key 的依賴
    let dep = depsMap.get(key)
    if (!dep) {
      // 如果 key 不存在,將這個 key 作為依賴收集起來,並初始化 value 為 new Set()
      depsMap.set(key, (dep = createDep()))
    }

    const eventInfo = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined

    trackEffects(dep, eventInfo)
  }
}

在 track 函數中,通過一個 if 語句判斷是否進行依賴收集,只有當 shouldTrack 為 true 並且存在 activeEffect,即開啟了依賴收集並且存在正在執行的副作用時,才進行依賴收集。

然後通過 target 物件從 targetMap 中嘗試獲取對應 target 的依賴集合depsMap,如果 targetMap 中不存在當前target的依賴集合,則將當前 target 新增進 targetMap 中,並將 targetMap 的 value 初始化為 new Map()。

// 在 targetMap 中獲取對應的 target 的依賴集合
let depsMap = targetMap.get(target)
if (!depsMap) {
  // 如果 target 不在 targetMap 中,則加入,並初始化 value 為 new Map()
  targetMap.set(target, (depsMap = new Map()))
}

接著根據target中被讀取的 key,從依賴集合depsMap中獲取對應 key 的依賴,如果依賴不存在,則將這個 key 的依賴收集到依賴集合depsMap中,並將依賴初始化為 new Set()。

// 從依賴集合中獲取對應的 key 的依賴
let dep = depsMap.get(key)
if (!dep) {
  // 如果 key 不存在,將這個 key 作為依賴收集起來,並初始化 value 為 new Set()
  depsMap.set(key, (dep = createDep()))
}

最後呼叫 trackEffects 函數,將副作用函數收集到依賴集合depsMap中。

const eventInfo = __DEV__
  ? { effect: activeEffect, target, type, key }
  : undefined

trackEffects(dep, eventInfo)

trackEffects 函數

// 收集副作用函數
export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit // set newly tracked
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    // 如果依賴中並不存當前的 effect 副作用函數
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    // 將當前的副作用函數收集進依賴中
    dep.add(activeEffect!)
    // 並在當前副作用函數的 deps 屬性中記錄該依賴
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        Object.assign(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo
        )
      )
    }
  }
}

在 trackEffects 函數中,檢查當前正在執行的副作用函數 activeEffect 是否已經被收集到依賴集合中,如果沒有,就將當前的副作用函數收集到依賴集合中。同時在當前副作用函數的 deps 屬性中記錄該依賴。

trigger 派發更新

當對屬性進行賦值時,會觸發代理物件的 set 攔截函數執行,如下面的程式碼所示:

const obj = { foo: 1 }

const p = new Proxy(obj, {
  // 攔截設定操作
  set(target, key, newVal, receiver){
    // 如果屬性不存在,則說明是在新增新屬性,否則設定已有屬性
    const type = Object.prototype.hasOwnProperty.call(target,key) ?  'SET' : 'ADD'
    
    // 設定屬性值
    const res = Reflect.set(target, key, newVal, receiver)
    // 把副作用函數從桶裡取出並執行,將 type 作為第三個引數傳遞給 trigger 函數
    trigger(target,key,type)
    
    return res
  }
  
  // 省略其他攔截函數
})

p.foo = 2

在上面的程式碼中,通過代理物件p 存取 foo 屬性,便會觸發 set 攔截函數的執行,此時就在 set 攔截函數中呼叫 trigger 函數中派發更新。原始碼中 set 攔截函數的解析可閱讀《Vue3 原始碼解讀之非原始值的響應式原理》一文中的「設定屬性操作的攔截」小節。

下面,我們來看看 track 函數的實現。

trigger 函數

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)
  // 該 target 從未被追蹤,則不繼續執行
  if (!depsMap) {
    // never been tracked
    return
  }

  // 存放所有需要派發更新的副作用函數  
  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    // 當需要清除依賴時,將當前 target 的依賴全部傳入
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    // 處理陣列的特殊情況
    depsMap.forEach((dep, key) => {
      // 如果對應的長度, 有依賴收集需要更新
      if (key === 'length' || key >= (newValue as number)) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    // 在 SET | ADD | DELETE 的情況,新增當前 key 的依賴
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            // 操作型別為 ADD 時觸發Map 資料結構的 keys 方法的副作用函數重新執行
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            // 操作型別為 DELETE 時觸發Map 資料結構的 keys 方法的副作用函數重新執行
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  const eventInfo = __DEV__
    ? { target, type, key, newValue, oldValue, oldTarget }
    : undefined

  if (deps.length === 1) {
    if (deps[0]) {
      if (__DEV__) {
        triggerEffects(deps[0], eventInfo)
      } else {
        triggerEffects(deps[0])
      }
    }
  } else {
    const effects: ReactiveEffect[] = []
    // 將需要執行的副作用函數收集到 effects 陣列中
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}

在 trigger 函數中,首先檢查當前 target 是否有被追蹤,如果從未被追蹤過,即target的依賴未被收集,則不需要執行派發更新,直接返回即可。

const depsMap = targetMap.get(target)
// 該 target 從未被追蹤,則不繼續執行
if (!depsMap) {
  // never been tracked
  return
}

接著建立一個 Set 型別的 deps 集合,用來儲存當前target的這個 key 所有需要執行派發更新的副作用函數。

// 存放所有需要派發更新的副作用函數  
let deps: (Dep | undefined)[] = []

接下來就根據操作型別type 和 key 來收集需要執行派發更新的副作用函數。

如果操作型別是 TriggerOpTypes.CLEAR ,那麼表示需要清除所有依賴,將當前target的所有副作用函數新增到 deps 集合中。

if (type === TriggerOpTypes.CLEAR) {
  // collection being cleared
  // trigger all effects for target
  // 當需要清除依賴時,將當前 target 的依賴全部傳入
  deps = [...depsMap.values()]
}

如果操作目標是陣列,並且修改了陣列的 length 屬性,需要把與 length 屬性相關聯的副作用函數以及索引值大於或等於新的 length 值元素的相關聯的副作用函數從 depsMap 中取出並新增到 deps 集合中。

else if (key === 'length' && isArray(target)) {
  // 如果操作目標是陣列,並且修改了陣列的 length 屬性
  depsMap.forEach((dep, key) => {
    // 對於索引大於或等於新的 length 值的元素,
    // 需要把所有相關聯的副作用函數取出並新增到 deps 中執行
    if (key === 'length' || key >= (newValue as number)) {
      deps.push(dep)
    }
  })
} 

如果當前的 key 不為 undefined,則將與當前key相關聯的副作用函數新增到 deps 集合中。注意這裡的判斷條件 void 0,是通過 void 運運算元的形式表示 undefined 。

if (key !== void 0) {
  deps.push(depsMap.get(key))
}

接下來通過 Switch 語句來收集操作型別為 ADD、DELETE、SET 時與 ITERATE_KEY 和 MAP_KEY_ITERATE_KEY 相關聯的副作用函數。

// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
  case TriggerOpTypes.ADD:
    if (!isArray(target)) {
      deps.push(depsMap.get(ITERATE_KEY))
      if (isMap(target)) {
        // 操作型別為 ADD 時觸發Map 資料結構的 keys 方法的副作用函數重新執行
        deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
      }
    } else if (isIntegerKey(key)) {
      // new index added to array -> length changes
      deps.push(depsMap.get('length'))
    }
    break
  case TriggerOpTypes.DELETE:
    if (!isArray(target)) {
      deps.push(depsMap.get(ITERATE_KEY))
      if (isMap(target)) {
        // 操作型別為 DELETE 時觸發Map 資料結構的 keys 方法的副作用函數重新執行
        deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
      }
    }
    break
  case TriggerOpTypes.SET:
    if (isMap(target)) {
      deps.push(depsMap.get(ITERATE_KEY))
    }
    break
}

最後呼叫 triggerEffects 函數,傳入收集的副作用函數,執行派發更新。

const eventInfo = __DEV__
  ? { target, type, key, newValue, oldValue, oldTarget }
  : undefined

if (deps.length === 1) {
  if (deps[0]) {
    if (__DEV__) {
      triggerEffects(deps[0], eventInfo)
    } else {
      triggerEffects(deps[0])
    }
  }
} else {
  const effects: ReactiveEffect[] = []
  // 將需要執行的副作用函數收集到 effects 陣列中
  for (const dep of deps) {
    if (dep) {
      effects.push(...dep)
    }
  }
  if (__DEV__) {
    triggerEffects(createDep(effects), eventInfo)
  } else {
    triggerEffects(createDep(effects))
  }
}

triggerEffects 函數

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  // 遍歷需要執行的副作用函數集合   
  for (const effect of isArray(dep) ? dep : [...dep]) {
    // 如果 trigger 觸發執行的副作用函數與當前正在執行的副作用函數相同,則不觸發執行
    if (effect !== activeEffect || effect.allowRecurse) {
      if (__DEV__ && effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
      }
      if (effect.scheduler) {
        // 如果一個副作用函數存在排程器,則呼叫該排程器
        effect.scheduler()
      } else {
        // 否則直接執行副作用函數
        effect.run()
      }
    }
  }
}

在 triggerEffects 函數中,遍歷需要執行的副作用函數集合,如果當前副作用函數存在排程器,則執行該排程器,否則直接執行該副作用函數的 run 方法,執行更新。

總結

本文深入分析了副作用的實現以及執行時機,並詳細分析了用於儲存副作用函數的targetMap的資料結構及其實現原理。還深入分析了依賴收集track函數以及派發更新 trigger 函數的實現。Vue 在追蹤變化時,通過 track 函數收集依賴,即將副作用函數新增到 targetMap 中,通過 trigger 函數來執行對應的副作用函來完成更新。

到此這篇關於Vue3 原始碼解讀之副作用函數與依賴收集的文章就介紹到這了,更多相關Vue3副作用函數與依賴收集內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com