溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Vue3響應(yīng)式核心之effect怎么使用

發(fā)布時間:2023-04-25 15:39:16 來源:億速云 閱讀:146 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要講解了“Vue3響應(yīng)式核心之effect怎么使用”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“Vue3響應(yīng)式核心之effect怎么使用”吧!

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

Vue3響應(yīng)式核心之effect怎么使用

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

一、effect用法

1、基本用法

const obj = reactive({count: 1})

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

obj.count++

結(jié)果會先打印1, 然后在obj.count++之后打印出2。

流程簡圖如下:

Vue3響應(yīng)式核心之effect怎么使用

運行effect(fun)

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

const runner = new ReactiveEffect(fn)

return runner

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

  }
}

console.log(obj.count)track依賴收集 結(jié)構(gòu)如下:

Vue3響應(yīng)式核心之effect怎么使用

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

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

所以又打印出2

2、lazy屬性為true

此值為 true 時,只有在第一次手動調(diào)用 runner 后,依賴數(shù)據(jù)變更時,才會自動執(zhí)行 effect 的回調(diào),可以理解為 effect 的是在手動調(diào)用 runner 后才首次執(zhí)行

const obj = reactive({count: 1})

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

只會打印出2

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

Vue3響應(yīng)式核心之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的打印結(jié)果:

Vue3響應(yīng)式核心之effect怎么使用

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

二、源碼分析

1、effect方法的實現(xiàn)

// 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, // 副作用函數(shù)
  options?: ReactiveEffectOptions // 結(jié)構(gòu)如上
): ReactiveEffectRunner {
  // 如果 fn 對象上有 effect 屬性
  if ((fn as ReactiveEffectRunner).effect) {
    // 那么就將 fn 替換為 fn.effect.fn
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  // 創(chuàng)建一個響應(yīng)式副作用函數(shù)
  const _effect = new ReactiveEffect(fn)
  if (options) {
    // 將配置項合并到響應(yīng)式副作用函數(shù)上
    extend(_effect, options)
    // 如果配置項中有 scope 屬性(該屬性的作用是指定副作用函數(shù)的作用域)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) { // options.lazy 不為true
    _effect.run() // 執(zhí)行響應(yīng)式副作用函數(shù) 首次執(zhí)行fn()
  }
  // _effect.run作用域綁定到_effect
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  // 將響應(yīng)式副作用函數(shù)賦值給 runner.effect
  runner.effect = _effect
  return runner
}

核心代碼:

創(chuàng)建一個響應(yīng)式副作用函數(shù)const _effect = new ReactiveEffect(fn),其運行結(jié)果如下:

Vue3響應(yīng)式核心之effect怎么使用

非lazy狀態(tài)執(zhí)行響應(yīng)式副作用函數(shù)_effect.run()

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

_effect.run作用域綁定到_effect

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

返回副作用函數(shù)runner

2、ReactiveEffect函數(shù)源碼

export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = [] // 響應(yīng)式依賴項的集合
  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
  ) {
    // 記錄當(dāng)前 ReactiveEffect 對象的作用域
    recordEffectScope(this, scope)
  }

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

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

  stop() {
    // stopped while running itself - defer the cleanup
    // 如果當(dāng)前 活動的 ReactiveEffect 對象是 “自己”
    // 延遲停止,需要執(zhí)行完當(dāng)前的副作用函數(shù)之后再停止
    if (activeEffect === this) {
      // 在 run 方法中會判斷 deferStop 的值,如果為 true,就會執(zhí)行 stop 方法
      this.deferStop = true
    } else if (this.active) {// 如果當(dāng)前 ReactiveEffect 對象處于活動狀態(tài)
      cleanupEffect(this) // 清除所有的依賴追蹤標(biāo)記
      if (this.onStop) { 
        this.onStop()
      }
      this.active = false // 將 active 設(shè)置為 false
    }
  }
}
  • run方法的作用就是執(zhí)行副作用函數(shù),并且在執(zhí)行副作用函數(shù)的過程中,會收集依賴;

  • stop方法的作用就是停止當(dāng)前的ReactiveEffect對象,停止之后,就不會再收集依賴了;

  • activeEffect和this并不是每次都相等的,因為activeEffect會跟著調(diào)用棧的深度而變化,而this則是固定的;

三、依賴收集相關(guān)

1、如何觸發(fā)依賴收集

在副作用函數(shù)中, obj.count就會觸發(fā)依賴收集

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

觸發(fā)的入口在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 觸發(fā)依賴的對象,例子中的obj
 * @param type 操作類型 比如obj.count就是get
 * @param key 指向?qū)ο蟮膋ey, 比如obj.count就是count
 */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) { // 是否應(yīng)該依賴收集 & 當(dāng)前的new ReactiveEffect()即指向的就是當(dāng)前正在執(zhí)行的副作用函數(shù)

    // 如果 targetMap 中沒有 target,就會創(chuàng)建一個 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對象,它指向的就是當(dāng)前正在執(zhí)行的副作用函數(shù);

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

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

targetMap的結(jié)構(gòu)偽代碼如下:

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

Vue3響應(yīng)式核心之effect怎么使用

以上是最原始的depMap

dev環(huán)境為增加響應(yīng)式調(diào)試會增加eventInfo

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

eventInfo結(jié)構(gòu)如下:

Vue3響應(yīng)式核心之effect怎么使用

trackEffects(dep, eventInfo)

如果 dep 中沒有當(dāng)前的 ReactiveEffect 對象,就會添加進去, 作用就把對象的屬性操作與副作用函數(shù)建立關(guān)聯(lián),接下來看trackEffects

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

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

      // 執(zhí)行之后 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 中沒有當(dāng)前的 ReactiveEffect 對象,就會添加進去

Vue3響應(yīng)式核心之effect怎么使用

最終生成的depTarget結(jié)構(gòu)如下:

Vue3響應(yīng)式核心之effect怎么使用

四、觸發(fā)依賴

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

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) // 觸發(fā)ADD依賴更新
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue) //觸發(fā)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中創(chuàng)建的依賴
  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結(jié)構(gòu)如下:

Vue3響應(yīng)式核心之effect怎么使用

執(zhí)行以上語句之后的depsMap結(jié)構(gòu)如下:

Vue3響應(yīng)式核心之effect怎么使用

將 depsMap 中 key 對應(yīng)的 ReactiveEffect 對象添加到 deps 中deps.push(depsMap.get(key))之后的deps結(jié)構(gòu)如下:

Vue3響應(yīng)式核心之effect怎么使用

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函數(shù)的作用就是觸發(fā)依賴,當(dāng)我們修改數(shù)據(jù)的時候,就會觸發(fā)依賴,然后執(zhí)行依賴中的副作用函數(shù)。

在這里的實現(xiàn)其實并沒有執(zhí)行,主要是收集一些需要執(zhí)行的副作用函數(shù),然后在丟給triggerEffects函數(shù)去執(zhí)行,接下來看看triggerEffects函數(shù)。

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

Vue3響應(yīng)式核心之effect怎么使用

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

3、triggerEffect(effect, debuggerEventExtraInfo)

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

effect.run()就是執(zhí)行副作用函數(shù)

感謝各位的閱讀,以上就是“Vue3響應(yīng)式核心之effect怎么使用”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對Vue3響應(yīng)式核心之effect怎么使用這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI