源碼分析Vue3響應式核心之effect

vue3響應式核心文章匯總:

vue3響應式核心之reactive源碼詳解

vue3響應式核心之effect源碼詳解

上篇vue3響應式核心之reactive源碼詳解詳細講解瞭reactive的原理和實現源碼,本篇文章將講解effect源碼。

通常情況下我們是不會直接使用effect的,因為effect是一個底層的API,在我們使用Vue3的時候Vue默認會幫我們調用effect。 effect翻譯為作用,意思是使其發生作用,這個使其的其就是我們傳入的函數,所以effect的作用就是讓我們傳入的函數發生作用,也就是執行這個函數。 執行過程簡圖如下:

接下來先通過例子瞭解effect的基本用法,然後再去瞭解原理。

一、effect用法

1、基本用法

const obj = reactive({count: 1})

const runner = effect(() => {
  console.log(obj.count)
})

obj.count++

結果會先打印1, 然後在obj.count++之後打印出2。

流程簡圖如下:

運行effect(fun)

// 先執行
fun()  // 打印出1

const runner = new ReactiveEffect(fn)

return runner

runner: {
  run() {
    this.fun() //執行fun
  },
  stop() {

  }
}

console.log(obj.count)track依賴收集 結構如下:

obj.count++觸發依賴,執行runner.run(), 實際運行的是

() => {
  console.log(obj.count)
}

所以又打印出2

2、lazy屬性為true

此值為 true 時,隻有在第一次手動調用 runner 後,依賴數據變更時,才會自動執行 effect 的回調,可以理解為 effect 的是在手動調用 runner 後才首次執行

const obj = reactive({count: 1})

const runner = effect(() => {
  console.log(obj.count)
}, {
  lazy: true
})
runner()
obj.count++

隻會打印出2

原因是effect源碼中有如下邏輯:

3、options中包含onTrack

let events = []
const onTrack = (e) => {
  events.push(e)
}
const obj = reactive({ foo: 1, bar: 2 })
const runner = effect(
  () => {
    console.log(obj.foo)
  },
  { onTrack }
)
console.log('runner', runner)
obj.foo++
console.log("events", events)

看下events的打印結果:

[
  {
    effect: runner,  // effect 函數的返回值
    target: toRaw(obj),  // 表示的是哪個響應式數據發生瞭變化
    type: TrackOpTypes.GET,  // 表示此次記錄操作的類型。 get 表示獲取值
    key: 'foo'
 }
]

二、源碼分析

1、effect方法的實現

// packages/reactivity/src/effect.ts
export interface ReactiveEffectOptions extends DebuggerOptions {
  lazy?: boolean
  scheduler?: EffectScheduler
  scope?: EffectScope
  allowRecurse?: boolean
  onStop?: () => void
}

export function effect<T = any>(
  fn: () => T, // 副作用函數
  options?: ReactiveEffectOptions // 結構如上
): ReactiveEffectRunner {
  // 如果 fn 對象上有 effect 屬性
  if ((fn as ReactiveEffectRunner).effect) {
    // 那麼就將 fn 替換為 fn.effect.fn
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  // 創建一個響應式副作用函數
  const _effect = new ReactiveEffect(fn)
  if (options) {
    // 將配置項合並到響應式副作用函數上
    extend(_effect, options)
    // 如果配置項中有 scope 屬性(該屬性的作用是指定副作用函數的作用域)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) { // options.lazy 不為true
    _effect.run() // 執行響應式副作用函數 首次執行fn()
  }
  // _effect.run作用域綁定到_effect
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  // 將響應式副作用函數賦值給 runner.effect
  runner.effect = _effect
  return runner
}

核心代碼:

創建一個響應式副作用函數const _effect = new ReactiveEffect(fn),其運行結果如下:

非lazy狀態執行響應式副作用函數_effect.run()

if (!options || !options.lazy) { // options.lazy 不為true
  _effect.run() // 執行響應式副作用函數 首次執行fn()
}

_effect.run作用域綁定到_effect

// _effect.run作用域綁定到_effect
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner

返回副作用函數runner

2、ReactiveEffect函數源碼

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
  /**
   * @internal
   */
  private deferStop?: 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
  ) {
    // 記錄當前 ReactiveEffect 對象的作用域
    recordEffectScope(this, scope)
  }

  run() {
    // 如果當前 ReactiveEffect 對象不處於活動狀態,直接返回 fn 的執行結果
    if (!this.active) {
      return this.fn()
    }
    // 尋找當前 ReactiveEffect 對象的最頂層的父級作用域
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack // 是否要跟蹤
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    try {
      // 記錄父級作用域為當前活動的 ReactiveEffect 對象
      this.parent = activeEffect
      activeEffect = this  // 將當前活動的 ReactiveEffect 對象設置為 “自己”
      shouldTrack = true // 將 shouldTrack 設置為 true (表示是否需要收集依賴)
      // effectTrackDepth 用於標識當前的 effect 調用棧的深度,執行一次 effect 就會將 effectTrackDepth 加 1
      trackOpBit = 1 << ++effectTrackDepth

      if (effectTrackDepth <= maxMarkerBits) {
        // 初始依賴追蹤標記
        initDepMarkers(this)
      } else {
        // 清除依賴追蹤標記
        cleanupEffect(this)
      }
      // 返回副作用函數執行結果
      return this.fn()
    } finally {
      // 如果 effect調用棧的深度 沒有超過閾值
      if (effectTrackDepth <= maxMarkerBits) {
        // 確定最終的依賴追蹤標記
        finalizeDepMarkers(this)
      }
      // 執行完畢會將 effectTrackDepth 減 1
      trackOpBit = 1 << --effectTrackDepth
      // 執行完畢,將當前活動的 ReactiveEffect 對象設置為 “父級作用域”
      activeEffect = this.parent
      // 將 shouldTrack 設置為上一個值
      shouldTrack = lastShouldTrack
      // 將父級作用域設置為 undefined
      this.parent = undefined
      // 延時停止,這個標志是在 stop 方法中設置的
      if (this.deferStop) {
        this.stop()
      }
    }
  }

  stop() {
    // stopped while running itself - defer the cleanup
    // 如果當前 活動的 ReactiveEffect 對象是 “自己”
    // 延遲停止,需要執行完當前的副作用函數之後再停止
    if (activeEffect === this) {
      // 在 run 方法中會判斷 deferStop 的值,如果為 true,就會執行 stop 方法
      this.deferStop = true
    } else if (this.active) {// 如果當前 ReactiveEffect 對象處於活動狀態
      cleanupEffect(this) // 清除所有的依賴追蹤標記
      if (this.onStop) { 
        this.onStop()
      }
      this.active = false // 將 active 設置為 false
    }
  }
}
  • run方法的作用就是執行副作用函數,並且在執行副作用函數的過程中,會收集依賴;
  • stop方法的作用就是停止當前的ReactiveEffect對象,停止之後,就不會再收集依賴瞭;
  • activeEffect和this並不是每次都相等的,因為activeEffect會跟著調用棧的深度而變化,而this則是固定的;

三、依賴收集相關

1、如何觸發依賴收集

在副作用函數中, obj.count就會觸發依賴收集

const runner = effect(() => {
  console.log(obj.count) 
})

觸發的入口在get攔截器裡面

function createGetter(isReadonly = false, shallow = false) {
  // 閉包返回 get 攔截器方法
  return function get(target: Target, key: string | symbol, receiver: object) {
    // ...
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    // ...
  }

2、track源碼

const targetMap = new WeakMap();
/**
 * 收集依賴
 * @param target target 觸發依賴的對象,例子中的obj
 * @param type 操作類型 比如obj.count就是get
 * @param key 指向對象的key, 比如obj.count就是count
 */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) { // 是否應該依賴收集 & 當前的new ReactiveEffect()即指向的就是當前正在執行的副作用函數

    // 如果 targetMap 中沒有 target,就會創建一個 Map
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep())) // createDep 生成dep = { w:0, n: 0}
    }

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

    trackEffects(dep, eventInfo)
  }
}

shouldTrack在上面也講過,它的作用就是控制是否收集依賴;

activeEffect就是我們剛剛講的ReactiveEffect對象,它指向的就是當前正在執行的副作用函數;

track方法的作用就是收集依賴,它的實現非常簡單,就是在targetMap中記錄下target和key;

targetMap是一個WeakMap,它的鍵是target,值是一個Map,這個Map的鍵是key,值是一個Set;

targetMap的結構偽代碼如下:

targetMap = {
  target: { 
    key: dep
  },
  // 比如:
  obj: { 
    count: {
       w: 0, 
       n: 0
    }
  }
}

以上是最原始的depMap

dev環境為增加響應式調試會增加eventInfo

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

eventInfo結構如下:

trackEffects(dep, eventInfo)

如果 dep 中沒有當前的 ReactiveEffect 對象,就會添加進去, 作用就把對象的屬性操作與副作用函數建立關聯,接下來看trackEffects

3、trackEffects(dep, eventInfo)源碼解讀

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      // 執行之前 dep = Set(0) {w: 0, n: 0}

      // 執行之後 dep = Set(0) {w: 0, n: 2}
      dep.n |= trackOpBit // set newly tracked
  
      shouldTrack = !wasTracked(dep) 
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    // 將activeEffect添加到dep
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) { // onTrack邏輯
      activeEffect!.onTrack(
        extend(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo!
        )
      )
    }
  }
}

dep.add(activeEffect!) 如果 dep 中沒有當前的 ReactiveEffect 對象,就會添加進去

最終生成的depTarget結構如下:

四、觸發依賴

比如例子中代碼obj.count++就會觸發set攔截,觸發依賴更新

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    //...
    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) {
        trigger(target, TriggerOpTypes.ADD, key, value) // 觸發ADD依賴更新
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue) //觸發SET依賴更新
      }
    }
    //...
  }

1、trigger依賴更新

// 路徑:packages/reactivity/src/effect.ts
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target) // 獲取depsMap, targetMap是在track中創建的依賴
  if (!depsMap) {
    // never been tracked
    return
  }

  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= newLength) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    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)) {
            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)) {
            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[] = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}

const depsMap = targetMap.get(target) 獲取 targetMap 中的 depsMap targetMap結構如下:

執行以上語句之後的depsMap結構如下:

將 depsMap 中 key 對應的 ReactiveEffect 對象添加到 deps 中deps.push(depsMap.get(key))之後的deps結構如下:

triggerEffects(deps[0], eventInfo)

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

trigger函數的作用就是觸發依賴,當我們修改數據的時候,就會觸發依賴,然後執行依賴中的副作用函數。

在這裡的實現其實並沒有執行,主要是收集一些需要執行的副作用函數,然後在丟給triggerEffects函數去執行,接下來看看triggerEffects函數。

2、triggerEffects(deps[0], eventInfo)

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

主要步驟

const effects = isArray(dep) ? dep : [...dep]獲取effects

triggerEffect(effect, debuggerEventExtraInfo)執行effect,接下來看看源碼

3、triggerEffect(effect, debuggerEventExtraInfo)

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
     // 如果 effect.onTrigger 存在,就會執行,隻有開發模式下才會執行
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    // 如果 effect 是一個調度器,就會執行 scheduler
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      // 其它情況執行 effect.run()
      effect.run()
    }
  }
}

effect.run()就是執行副作用函數

以上就是源碼分析Vue3響應式核心之effect的詳細內容,更多關於Vue3 effect的資料請關註WalkonNet其它相關文章!

推薦閱讀: