Vue3系列之effect和ReactiveEffect?track?trigger源碼解析
引言
介紹幾個(gè)API
的時(shí)候,我們發(fā)現(xiàn)里面常出現(xiàn)effect
、track
和trigger
,雖然簡(jiǎn)單說了下track
用于依賴收集,trigger
來觸發(fā)更新。但是畢竟沒看到具體實(shí)現(xiàn),心里沒底。如今便可以一探究竟。
一、ReactiveEffect
1. 相關(guān)的全局變量
之前提到的effect
,便是ReactiveEffect
的實(shí)例。用到了一些重要的全局變量。
targetMap
:弱映射,以目標(biāo)對(duì)象target
為key
,其收集到的依賴集depsMap
為值,因此通過目標(biāo)對(duì)象target
可以獲取到對(duì)應(yīng)的所有依賴;activeEffect
:當(dāng)前活躍的effect
,隨后會(huì)被收集起來;shouldTrack
:用作暫停和恢復(fù)依賴收集的標(biāo)志;trackStack
:歷史shouldTrack
的記錄棧。
targetMap
對(duì)比reactive
篇章中提到的proxyMap
:
- 兩者都是弱映射;
- 都以目標(biāo)對(duì)象
target
為key
; targetMap
全局只有一個(gè);而proxyMap
有四種,分別對(duì)應(yīng)reactive
、shallowReactive
、readonly
、shallowReadonly
;- 一個(gè)
target
在一種proxyMap
中最多只有一個(gè)對(duì)應(yīng)的代理proxy
,因此proxyMap
的值為單個(gè)的proxy
對(duì)象; - 一個(gè)
target
可以由很多的依賴dep
,因此targetMap
的值為數(shù)據(jù)集Map
。
const targetMap = new WeakMap<any, KeyToDepMap>() export let activeEffect: ReactiveEffect | undefined export let shouldTrack = true const trackStack: boolean[] = []
以及控制暫停、恢復(fù)依賴收集的函數(shù):
// 暫停收集 export function pauseTracking() { trackStack.push(shouldTrack) shouldTrack = false } // 恢復(fù)收集 export function enableTracking() { trackStack.push(shouldTrack) shouldTrack = true } // 重置為上一次的狀態(tài) export function resetTracking() { const last = trackStack.pop() shouldTrack = last === undefined ? true : last }
2. class 聲明
在構(gòu)造器中初始化fn
( 執(zhí)行run()
的過程中調(diào)用 ) 、調(diào)度器scheduler
,并通過recordEffectScope
來記錄實(shí)例的作用域;聲明一些實(shí)例屬性,以及run
、stop
兩個(gè)方法:
active
:boolean
類型,表示當(dāng)前的effect
是否起作用;deps
:當(dāng)前effect
的依賴;parent
:指向上一個(gè)活躍的effect
,形成鏈表;computed
:可選,在computed
函數(shù)得到的ComputedRefImpl
里的effect
具有這個(gè)屬性;allowRecurse
,可選,表示是否允許自調(diào)用;deferStop
:私有,可選,表示stop()
是否延遲執(zhí)行;onStop
:可選,函數(shù),在執(zhí)行stop()
時(shí)會(huì)調(diào)用onStop
;onTrack
onTrigger
:這兩個(gè)listener
為調(diào)試用,分別在依賴收集和響應(yīng)式更新時(shí)觸發(fā);- run:
effect
最核心的方法。 stop
:調(diào)用cleanupEffect
讓effect
停止起作用,如果是stop
當(dāng)前活躍的effect
,也就是自己停止自己,則會(huì)將deferStop
調(diào)為true
,從而延遲停止的時(shí)機(jī);觸發(fā)onStop
;將active
調(diào)為false
。
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 ) { recordEffectScope(this, scope) } run() { if (!this.active) { return this.fn() } // 當(dāng)前活躍的effect let parent: ReactiveEffect | undefined = activeEffect let lastShouldTrack = shouldTrack // 如果當(dāng)前活躍的effect就是這個(gè)effect本身,則直接返回 while (parent) { if (parent === this) { return } parent = parent.parent } // 依次活躍的effect形成鏈表,由parent屬性連接 try { this.parent = activeEffect activeEffect = this shouldTrack = true trackOpBit = 1 << ++effectTrackDepth if (effectTrackDepth <= maxMarkerBits) { // 遍歷 this.deps 將其中的effect設(shè)置為已捕獲 tracked initDepMarkers(this) } else { // 層級(jí)溢出則清除當(dāng)前副作用 cleanupEffect(this) } // 尾調(diào)用傳入的fn return this.fn() } finally { // 因?yàn)榍懊嬗衦eturn,因此當(dāng) try 的代碼塊發(fā)生異常時(shí)執(zhí)行 if (effectTrackDepth <= maxMarkerBits) { // 該方法遍歷 this.deps,將其中過氣的effect刪除,未捕獲的effect加入 // effect 就是其中的 dep finalizeDepMarkers(this) } trackOpBit = 1 << --effectTrackDepth // 復(fù)原一些狀態(tài) activeEffect = this.parent shouldTrack = lastShouldTrack this.parent = undefined // 若設(shè)置了延遲停止,則執(zhí)行stop,進(jìn)行延遲清理 if (this.deferStop) { this.stop() } } } // 清除副作用 stop() { // stopped while running itself - defer the cleanup if (activeEffect === this) { this.deferStop = true } else if (this.active) { cleanupEffect(this) if (this.onStop) { this.onStop() } this.active = false } } }
3. cleanupEffect
cleanupEffect
用于清除副作用。接收一個(gè)effect
,遍歷effect.deps
,并逐個(gè)刪除副作用effect
。隨后清空effect.deps
。
function cleanupEffect(effect: ReactiveEffect) { const { deps } = effect if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 } }
二、effect 函數(shù)
1. 相關(guān)ts類型
effect
函數(shù)有幾個(gè)相關(guān)的類型:
ReactiveEffectOptions
:effect
函數(shù)的入?yún)㈩愋椭唬?/li>ReactiveEffectRunner
:是一個(gè)函數(shù),且具有effect
屬性的類型;
export interface DebuggerOptions { onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void } export interface ReactiveEffectOptions extends DebuggerOptions { lazy?: boolean scheduler?: EffectScheduler scope?: EffectScope allowRecurse?: boolean onStop?: () => void } export interface ReactiveEffectRunner<T = any> { (): T effect: ReactiveEffect }
2. 函數(shù)聲明
effect
函數(shù)有兩個(gè)入?yún)ⅲ?/p>
fn
:是一個(gè)函數(shù),經(jīng)處理后用于創(chuàng)建ReactiveEffect
實(shí)例_effect
;options
:可選,用于覆蓋_effect
上的屬性。
export function effect<T = any>( fn: () => T, options?: ReactiveEffectOptions ): ReactiveEffectRunner { // 處理fn if ((fn as ReactiveEffectRunner).effect) { fn = (fn as ReactiveEffectRunner).effect.fn } // 根據(jù) fn 創(chuàng)建一個(gè) _effect const _effect = new ReactiveEffect(fn) if (options) { // 用 options 覆蓋 _effect 上的屬性 extend(_effect, options) if (options.scope) recordEffectScope(_effect, options.scope) } // 沒有 lazy , 則 _effect 立即執(zhí)行一次 run() if (!options || !options.lazy) { _effect.run() } // runner:拿到 _effect.run 并掛上 effect 屬性,包裝成 ReactiveEffectRunner 類型 const runner = _effect.run.bind(_effect) as ReactiveEffectRunner // effect屬性指回 _effect 自身,方便使用 runner 調(diào)用 run 和 stop runner.effect = _effect // 返回 runner return runner }
3. stop函數(shù)
stop
用于清除effect
。入?yún)?code>ReactiveEffectRunner;
export function stop(runner: ReactiveEffectRunner) { runner.effect.stop() }
三、track 依賴收集
1. track
一直在說track
進(jìn)行依賴收集,這里看下它到底怎么做的。
- 以目標(biāo)對(duì)象
target
為key
,depsMap
為targetMap
的值;以target
的key
為key
,使用createDep()
創(chuàng)建依賴dep
為值,存放在target
對(duì)應(yīng)的depsMap
中。 - 通過
trackEffects(dep, eventInfo)
來收集副作用。
// 全局變量 targetMap const targetMap = new WeakMap<any, KeyToDepMap>() export function track(target: object, type: TrackOpTypes, key: unknown) { if (shouldTrack && activeEffect) { 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())) } const eventInfo = __DEV__ ? { effect: activeEffect, target, type, key } : undefined trackEffects(dep, eventInfo) } }
2. createDep
使用createDep
創(chuàng)建一個(gè)新的dep
。可以看到,dep
是個(gè)Set
實(shí)例,且添加了兩個(gè)屬性:
w
:wasTracked
的首字母,表示當(dāng)前依賴是否被收集;n
:newlyTracked
的首字母,表示當(dāng)前依賴是否是新收集的。
export const createDep = (effects?: ReactiveEffect[]): Dep => { const dep = new Set<ReactiveEffect>(effects) as Dep dep.w = 0 dep.n = 0 return dep }
3. trackEffects
trackEffects
用于收集副作用。主要把當(dāng)前活躍的activeEffect
加入dep
,以及在activeEffect.deps
中加入該副作用影響到的所有依賴。
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. shouldTrack = !dep.has(activeEffect!) } // 當(dāng)前依賴 dep 還未被捕獲 / 當(dāng)前依賴 dep 中,還沒有當(dāng)前活躍的副作用時(shí), // 將當(dāng)前活躍的副作用 effect 添加進(jìn) dep 里,同時(shí)在把 dep 加入受副作用影響的依賴集合 activeEffect.deps 中 if (shouldTrack) { dep.add(activeEffect!) activeEffect!.deps.push(dep) if (__DEV__ && activeEffect!.onTrack) { activeEffect!.onTrack({ effect: activeEffect!, ...debuggerEventExtraInfo! }) } } }
4. 小結(jié)
用一句比較拗口的話來說,依賴收集就是把當(dāng)前活躍的副作用activeEffect
存入全局變量targetMap
中的 ( target
對(duì)應(yīng)的 depsMap
) 中 (target
的key
)對(duì)應(yīng)的 dep
( 類型為Set
) 中,并把這個(gè)dep
加入到受activeEffect
副作用影響的所有依賴activeEffect.deps
列表中。
四、trigger
觸發(fā)更新實(shí)際上就是觸發(fā)副作用,因此這一小節(jié)決定以與track
相反的順序來介紹。
1. triggerEffect
triggerEffect
觸發(fā)副作用從而更新。當(dāng)觸發(fā)更新的副作用effect
允許自調(diào)用,且不是當(dāng)前活躍的副作用時(shí),通過調(diào)度器scheduler
執(zhí)行副作用或者直接執(zhí)行run
,是實(shí)際上觸發(fā)更新的地方。
function triggerEffect( effect: ReactiveEffect, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { if (effect !== activeEffect || effect.allowRecurse) { if (__DEV__ && effect.onTrigger) { effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) } // 實(shí)際觸發(fā)更新的地方 if (effect.scheduler) { effect.scheduler() } else { effect.run() } } }
2. triggerEffects
接收一個(gè)dep
和用于調(diào)試的額外信息。遍歷dep
中的effect
,逐一使用triggerEffect
來執(zhí)行副作用。源碼在這里有點(diǎn)蜜汁操作。
export function triggerEffects( dep: Dep | ReactiveEffect[], debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { // spread into array for stabilization const effects = isArray(dep) ? dep : [...dep] // 兩者互斥,但是執(zhí)行的操作相同?而且為什么不寫在一個(gè) for...of... 里 ? for (const effect of effects) { if (effect.computed) { triggerEffect(effect, debuggerEventExtraInfo) } } for (const effect of effects) { if (!effect.computed) { triggerEffect(effect, debuggerEventExtraInfo) } } }
3. trigger
之前一直說trigger
觸發(fā)更新,其實(shí)是現(xiàn)在已經(jīng)知道了,實(shí)際是triggerEffect
來執(zhí)行副作用從而實(shí)現(xiàn)更新。
這里是創(chuàng)建一個(gè)deps
數(shù)組,根據(jù)target
、key
和觸發(fā)更新的操作類型type
等參數(shù),來獲取所有的相關(guān)dep
,放入deps
。再取出deps
中所有的dep
里的所有effect
,放入effects
列表中,通過triggerEffects(effects)
來觸發(fā)所有的相關(guān)副作用,最終實(shí)現(xiàn)更新。
需要注意的是對(duì)于數(shù)組:
- 修改
length
屬性會(huì)導(dǎo)致該數(shù)組所有依賴的更新; - 修數(shù)組新增成員會(huì)引起
length
屬性相關(guān)的依賴的更新,因?yàn)?code>length的值發(fā)生了變化。
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 } // 用于聚集所有相關(guān)依賴 let deps: (Dep | undefined)[] = [] if (type === TriggerOpTypes.CLEAR) { // 調(diào)用了Set、Map實(shí)例的clear方法,將觸發(fā)全部相關(guān)的副作用 // collection being cleared // trigger all effects for target deps = [...depsMap.values()] } else if (key === 'length' && isArray(target)) { // 目標(biāo)對(duì)象是數(shù)組,且修改了length屬性時(shí),會(huì)觸發(fā)全部相關(guān)的副作用 depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { 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)) { // 數(shù)組下標(biāo)成員的更改 會(huì)引起 length 屬性相關(guān)的更新 // 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) } } // 這里triggerEffects接受的參數(shù)類型為Set,之前的是數(shù)組 if (__DEV__) { triggerEffects(createDep(effects), eventInfo) } else { triggerEffects(createDep(effects)) } } }
五、小結(jié)
1. 依賴收集
targetMap
中有depsMap
(以target
為key
);depsMap
中有許多dep
(以targetMap
的key
為key
);簡(jiǎn)單理解為:在編譯時(shí)根據(jù)target
和key
,創(chuàng)建副作用,將activeEffect
指向新建的副作用,并存放到相關(guān)的依賴dep
里的過程就是依賴收集。
2. 觸發(fā)更新
反過來,觸發(fā)target
、key
相關(guān)的dep
中所有相關(guān)的副作用,通過各個(gè)副作用上的effect.scheduler()
或者effect.run()
來實(shí)現(xiàn)更新。
以上就是Vue3系列之effect和ReactiveEffect track trigger源碼解析的詳細(xì)內(nèi)容,更多關(guān)于Vue3 effect和ReactiveEffect track trigger的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
前端vue3手動(dòng)設(shè)置滾動(dòng)條位置/自動(dòng)定位詳細(xì)代碼
這篇文章主要給大家介紹了關(guān)于前端vue3手動(dòng)設(shè)置滾動(dòng)條位置/自動(dòng)定位的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)學(xué)習(xí)或者使用vue3具有一定的參考解決價(jià)值,需要的朋友可以參考下2024-05-05Vue實(shí)現(xiàn)固定定位圖標(biāo)滑動(dòng)隱藏效果
移動(dòng)端頁面,有時(shí)候會(huì)出現(xiàn)一些固定定位在底部圖標(biāo),比如購物車等。這篇文章主要介紹了Vue制作固定定位圖標(biāo)滑動(dòng)隱藏效果,需要的朋友可以參考下2019-05-05Vue3?使用v-model實(shí)現(xiàn)父子組件通信的方法(常用在組件封裝規(guī)范中)
這篇文章主要介紹了Vue3?使用v-model實(shí)現(xiàn)父子組件通信(常用在組件封裝規(guī)范中)的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-06-06vue源碼解讀子節(jié)點(diǎn)優(yōu)化更新
這篇文章主要為大家介紹了vue源碼解讀子節(jié)點(diǎn)優(yōu)化更新示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08用v-html解決Vue.js渲染中html標(biāo)簽不被解析的問題
這篇文章主要給大家介紹了如何利用v-html解決Vue.js渲染過程中html標(biāo)簽不能被解析,html標(biāo)簽顯示為字符串的問題,文中通過圖文介紹的很詳細(xì),有需要的朋友們可以參考借鑒,下面來一起看看吧。2016-12-12vue+elementUI 復(fù)雜表單的驗(yàn)證、數(shù)據(jù)提交方案問題
這篇文章主要介紹了vue+elementUI 復(fù)雜表單的驗(yàn)證、數(shù)據(jù)提交方案,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-06-06Vue2?中的數(shù)據(jù)劫持簡(jiǎn)寫示例
這篇文章主要為大家介紹了Vue2?中的數(shù)據(jù)劫持簡(jiǎn)寫示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02