您好,登錄后才能下訂單哦!
這篇“Vue3之副作用函數與依賴收集實例分析”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“Vue3之副作用函數與依賴收集實例分析”文章吧。
副作用函數是指會產生副作用的函數,如下面的代碼所示:
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 是一個 WeakMap 類型的集合,用來存儲副作用函數,從類型定義可以看出 targetMap的數據結構方式:
WeakMap 由 target --> Map
構成
Map 由 key --> Set
構成
其中 WeakMap 的鍵是原始對象 target,WeakMap 的值是一個 Map 實例,Map 的鍵是原始對象 target 的 key,Map 的值是一個由副作用函數組成的 Set。它們的關系如下:
我們來看下面的代碼:
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 變量用來維護當前正在執行的副作用
shouldTrack 變量用來標識是否開啟依賴搜集,只有 shouldTrack 的值為 true 時,才進行依賴收集,即將副作用函數添加到依賴集合中。
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 類的實現。
// 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 方法,該方法用來停止并清除當前正在執行的副作用。
當使用代理對象訪問對象的屬性時,就會觸發代理對象的 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 函數的實現。
// 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)
// 收集副作用函數 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 屬性中記錄該依賴。
當對屬性進行賦值時,會觸發代理對象的 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 函數的源碼如下:
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)) } }
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 方法,執行更新。
以上就是關于“Vue3之副作用函數與依賴收集實例分析”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。