【淺談Vue3 effect】

Vue3 中引入了 proxy進(jìn)行數(shù)據(jù)劫持,而effect是響應(yīng)式系統(tǒng)的核心,而響應(yīng)式系統(tǒng)又是 vue3 中的核心,所以從 effect 開始講起。
首先看下面 effect 的傳參,fn 是回調(diào)函數(shù),options 是傳入的參數(shù)。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

其中 option 的參數(shù)如下,都是屬于可選的。

參數(shù) 含義
lazy 是否延遲觸發(fā) effect
computed 是否為計算屬性
scheduler 調(diào)度函數(shù)
onTrack 追蹤時觸發(fā)
onTrigger 觸發(fā)回調(diào)時觸發(fā)
onStop 停止監(jiān)聽時觸發(fā)
export interface ReactiveEffectOptions {
  lazy?: boolean
  computed?: boolean
  scheduler?: (job: ReactiveEffect) => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  onStop?: () => void
}

分析完參數(shù)之后,繼續(xù)我們一開始的分析。當(dāng)我們調(diào)用 effect 時,首先判斷傳入的 fn 是否是 effect,如果是,取出原始值,然后調(diào)用 createReactiveEffect 創(chuàng)建 新的effect, 如果傳入的 option 中的 lazy 不為為 true,則立即調(diào)用我們剛剛創(chuàng)建的 effect, 最后返回剛剛創(chuàng)建的 effect。
那么 createReactiveEffect是怎樣是創(chuàng)建 effect的呢?

function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  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 {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

我們先忽略 reactiveEffect,繼續(xù)看下面的掛載的屬性。

effect 掛載屬性 含義
id 自增id, 唯一標(biāo)識effect
_isEffect 用于標(biāo)識方法是否是effect
active effect 是否激活
raw 創(chuàng)建effect是傳入的fn
deps 持有當(dāng)前 effect 的dep 數(shù)組
options 創(chuàng)建effect是傳入的options

回到 reactiveEffect,如果 effect 不是激活狀態(tài),這種情況發(fā)生在我們調(diào)用了 effect 中的 stop 方法之后,那么先前沒有傳入調(diào)用 scheduler 函數(shù)的話,直接調(diào)用原始方法fn,否則直接返回。
那么處于激活狀態(tài)的 effect 要怎么進(jìn)行處理呢?首先判斷是否當(dāng)前 effect 是否在 effectStack 當(dāng)中,如果在,則不進(jìn)行調(diào)用,這個主要是為了避免死循環(huán)。拿下面測試用例來看

it('should avoid infinite loops with other effects', () => {
    const nums = reactive({ num1: 0, num2: 1 })

    const spy1 = jest.fn(() => (nums.num1 = nums.num2))
    const spy2 = jest.fn(() => (nums.num2 = nums.num1))
    effect(spy1)
    effect(spy2)
    expect(nums.num1).toBe(1)
    expect(nums.num2).toBe(1)
    expect(spy1).toHaveBeenCalledTimes(1)
    expect(spy2).toHaveBeenCalledTimes(1)
    nums.num2 = 4
    expect(nums.num1).toBe(4)
    expect(nums.num2).toBe(4)
    expect(spy1).toHaveBeenCalledTimes(2)
    expect(spy2).toHaveBeenCalledTimes(2)
    nums.num1 = 10
    expect(nums.num1).toBe(10)
    expect(nums.num2).toBe(10)
    expect(spy1).toHaveBeenCalledTimes(3)
    expect(spy2).toHaveBeenCalledTimes(3)
})

如果不加 effectStack,會導(dǎo)致 num2 改變,觸發(fā)了 spy1, spy1 里面 num1 改變又觸發(fā)了 spy2, spy2 又會改變 num2,從而觸發(fā)了死循環(huán)。

接著是清除依賴,每次 effect 運(yùn)行都會重新收集依賴, deps 是持有 effect 的依賴數(shù)組,其中里面的每個 dep 是對應(yīng)對象某個 key 的 全部依賴,我們在這里需要做的就是首先把 effect 從 dep 中刪除,最后把 deps 數(shù)組清空。

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
  }
}

清除完依賴,就開始重新收集依賴。首先開啟依賴收集,把當(dāng)前 effect 放入 effectStack 中,然后講 activeEffect 設(shè)置為當(dāng)前的 effect,activeEffect 主要為了在收集依賴的時候使用(在下面會很快講到),然后調(diào)用 fn 并且返回值,當(dāng)這一切完成的時候,finally 階段,會把當(dāng)前 effect 彈出,恢復(fù)原來的收集依賴的狀態(tài),還有恢復(fù)原來的 activeEffect。

 try {
    enableTracking()
    effectStack.push(effect)
    activeEffect = effect
    return fn(...args)
  } finally {
    effectStack.pop()
    resetTracking()
    activeEffect = effectStack[effectStack.length - 1]
  }

那 effect 是怎么收集依賴的呢?vue3 利用 proxy 劫持對象,在上面運(yùn)行 effect 中讀取對象的時候,當(dāng)前對象的 key 的依賴 set集合 會把 effect 收集進(jìn)去。

export function track(target: object, type: TrackOpTypes, key: unknown) {
  ...
}

vue3 在 reactive 中觸發(fā) track 函數(shù),reactive 會在單獨(dú)的章節(jié)講。觸發(fā) track 的參數(shù)中,object 表示觸發(fā) track 的對象, type 代表觸發(fā) track 類型,而 key 則是 觸發(fā) track 的 object 的 key。在下面可以看到三種類型的讀取對象會觸發(fā) track,分別是 get、 has、 iterate。

export const enum TrackOpTypes {
  GET = 'get',
  HAS = 'has',
  ITERATE = 'iterate'
}

回到 track 內(nèi)部,如果 shouldTrack 為 false 或者 activeEffect 為空,則不進(jìn)行依賴收集。接著 targetMap 里面有沒有該對象,沒有新建 map,然后再看這個 map 有沒有這個對象的對應(yīng) key 的 依賴 set 集合,沒有則新建一個。 如果對象對應(yīng)的 key 的 依賴 set 集合也沒有當(dāng)前 activeEffect, 則把 activeEffect 加到 set 里面,同時把 當(dāng)前 set 塞到 activeEffect 的 deps 數(shù)組。最后如果是開發(fā)環(huán)境而且傳入了 onTrack 函數(shù),則觸發(fā) onTrack。
所以 deps 就是 effect 中所依賴的 key 對應(yīng)的 set 集合數(shù)組, 畢竟一般來說,effect 中不止依賴一個對象或者不止依賴一個對象的一個key,而且 一個對象可以能不止被一個 effect 使用,所以是 set 集合數(shù)組。

if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  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)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }

依賴都收集完畢了,接下來就是觸發(fā)依賴。如果 targetMap 為空,說明這個對象沒有被追蹤,直接return。

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
  }
  ...
}

其中觸發(fā)的 type, 包括了 set、add、delete 和 clear。

export const enum TriggerOpTypes {
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear'
}

接下來對 key 收集的依賴進(jìn)行分組,computedRunners 具有更高的優(yōu)先級,會觸發(fā)下游的 effects 重新收集依賴,

const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()

add 方法是將 effect 添加進(jìn)不同分組的函數(shù),其中 effect !== activeEffect 這個是為了避免死循環(huán),在下面的注釋也寫的很清楚,避免出現(xiàn) foo.value++ 這種情況。至于為什么是 set 呢,要避免 effect 多次運(yùn)行。就好像循環(huán)中,set 觸發(fā)了 trigger ,那么 ITERATE 和 當(dāng)前 key 可能都屬于同個 effect,這樣就可以避免多次運(yùn)行了。

const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
  effectsToAdd.forEach(effect => {
    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
    }
  })
}
}

下面根據(jù)觸發(fā) key 類型的不同進(jìn)行 effect 的處理。如果是 clear 類型,則觸發(fā)這個對象所有的 effect。如果 key 是 length , 而且 target 是數(shù)組,則會觸發(fā) key 為 length 的 effects ,以及 key 大于等于新 length的 effects, 因為這些此時數(shù)組長度變化了。

if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
} 

下面則是對正常的新增、修改、刪除進(jìn)行 effect 的分組, isAddOrDelete 表示新增 或者不是數(shù)組的刪除,這為了對迭代 key的 effect 進(jìn)行觸發(fā),如果 isAddOrDelete 為 true 或者是 map 對象的設(shè)值,則觸發(fā) isArray(target) ? 'length' : ITERATE_KEY 的 effect ,如果 isAddOrDelete 為 true 且 對象為 map, 則觸發(fā) MAP_KEY_ITERATE_KEY 的 effect

else {
    // schedule runs for SET | ADD | DELETE
    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))
    }
}

最后是運(yùn)行 effect, 像上面所說的,computed effects 會優(yōu)先運(yùn)行,因為 computed effects 在運(yùn)行過程中,第一次會觸發(fā)上游把cumputed effect收集進(jìn)去,再把下游 effect 收集起來。

還有一點(diǎn),就是 effect.options.scheduler,如果傳入了調(diào)度函數(shù),則通過 scheduler 函數(shù)去運(yùn)行 effect, 但是 scheduler 里面可能不一定使用了 effect,例如 computed 里面,因為 computed 是延遲運(yùn)行 effect, 這個會在講 computed 的時候再講。

const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.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)

可以發(fā)現(xiàn),不管是 track 還是 trigger, 都會導(dǎo)致 effect 重新運(yùn)行去收集依賴。

最后再講一個 stop 方法,當(dāng)我們調(diào)用 stop 方法后,會清空其他對象對 effect 的依賴,同時調(diào)用 onStop 回調(diào),最后將 effect 的激活狀態(tài)設(shè)置為 false

export function stop(effect: ReactiveEffect) {
  if (effect.active) {
    cleanup(effect)
    if (effect.options.onStop) {
      effect.options.onStop()
    }
    effect.active = false
  }
}

這樣當(dāng)再一次調(diào)用 effect 的時候,不會進(jìn)行依賴的重新收集,而且沒有調(diào)度函數(shù),就直接返回原始的 fn 的運(yùn)行結(jié)果,否則直接返回 undefined。

if (!effect.active) {
  return options.scheduler ? undefined : fn(...args)
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容