關(guān)于Vue3中的響應(yīng)式原理
一、簡介
本章內(nèi)容主要通過具體的簡單示例來分析Vue3是如何實現(xiàn)響應(yīng)式的。理解本章需要了解Vue3的響應(yīng)式對象。只注重原理設(shè)計層面,細節(jié)不做太多講解。
二、響應(yīng)核心
1.核心源碼
export class ReactiveEffect<T = any> { //是否激活 active = true //依賴列表 deps: Dep[] = [] // can be attached after creation computed?: boolean //是否允許遞歸 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 | null ) { //將自身添加到一個全局的EffectScope容器中 recordEffectScope(this, scope) } run() { if (!this.active) { //沒有激活,直接調(diào)用回調(diào)方法 return this.fn() } //棧中不存在當前對象 if (!effectStack.includes(this)) { try { //推入棧頂,并且置為全局激活對象 effectStack.push((activeEffect = this)) //開啟依賴收集開關(guān) enableTracking() //位操作符:用于優(yōu)化 trackOpBit = 1 << ++effectTrackDepth //源碼中maxMarkerBits取30 猜測是因為整數(shù)位運算時是按照32位計算 當1<<31時為負值了,后續(xù)負值的位運算得不到預期結(jié)果 所以取的最大30 if (effectTrackDepth <= maxMarkerBits) { //將當前依賴列表的所有依賴置為"已經(jīng)收集" initDepMarkers(this) } else { //不采用優(yōu)化模式,使用老流程,直接移除依賴的全部狀態(tài) cleanupEffect(this) } //調(diào)用回調(diào) return this.fn() } finally { if (effectTrackDepth <= maxMarkerBits) { //斷掉依賴關(guān)聯(lián) finalizeDepMarkers(this) } //重置位操作狀態(tài) trackOpBit = 1 << --effectTrackDepth //重置依賴收集狀態(tài) resetTracking() //棧頂出棧 effectStack.pop() const n = effectStack.length activeEffect = n > 0 ? effectStack[n - 1] : undefined } } } stop() { if (this.active) { cleanupEffect(this) if (this.onStop) { this.onStop() } this.active = false } } }
上述ReactiveEffect對象,其實需要關(guān)注的就是一個run方法,這個方法設(shè)計得十分巧妙,所有動態(tài)響應(yīng)的本質(zhì)其實都是通過調(diào)用run方法實現(xiàn)的。
比如如下代碼:
let dummy const counter = reactive({ num: 0 }) let innerfunc = () => dummy = counter.num; effect(innerfunc) //下面的賦值,最終會執(zhí)行innerfunc,所以dummy會變成7 counter.num = 7
可能會有疑惑,上述代碼并沒有出現(xiàn)ReactiveEffect類型的對象,它其實是在effect方法中創(chuàng)建的,我們接下來分析下effect方法。
export function effect<T = any>( fn: () => T, options?: ReactiveEffectOptions ): ReactiveEffectRunner { if ((fn as ReactiveEffectRunner).effect) { fn = (fn as ReactiveEffectRunner).effect.fn } //創(chuàng)建對象并傳參 const _effect = new ReactiveEffect(fn) if (options) { extend(_effect, options) if (options.scope) recordEffectScope(_effect, options.scope) } if (!options || !options.lazy) { //執(zhí)行 _effect.run() } const runner = _effect.run.bind(_effect) as ReactiveEffectRunner runner.effect = _effect return runner }
這個方法的簡單用法很簡單,就是創(chuàng)建一個ReactiveEffect類型對象,然后執(zhí)行run方法。
可能對于recordEffectScope方法有疑惑,其實這個方法和響應(yīng)式無關(guān),它的主要作用是將一個ReactiveEffect對象放入一個effectScope容器對象內(nèi),這個容器對象可以方便快捷的對容器內(nèi)所有的ReactiveEffect對象和其子effectScope調(diào)用stop方法。只關(guān)注響應(yīng)式的話可以不作考慮。
2.逐步分析上述示例代碼
let dummy //步驟1:創(chuàng)建一個響應(yīng)式對象 const counter = reactive({ num: 0 }) let innerfunc = () => dummy = counter.num; //步驟2:調(diào)用effect方法 effect(innerfunc) //步驟3:修改響應(yīng)式對象數(shù)據(jù) counter.num = 7
上述的測試代碼看似就3個步驟,其實內(nèi)部做的東西非常多,我們來跟蹤下運行流程。
步驟1:這一步很簡單,就是單純的創(chuàng)建一個Proxy對象,此時counter對象變成響應(yīng)式的。
步驟2:effect方法里面最終調(diào)用的是run方法,而run方法主要是將自身掛載到全局激活并入棧,此時調(diào)用回調(diào)方法。回調(diào)方法此時為上面innerfunc方法,調(diào)用這個方法會讀取counter.num屬性,讀取響應(yīng)式對象的屬性會調(diào)用代理攔截處理的get方法,在get方法里面,會收集依賴。此時將依賴存于棧頂?shù)哪莻€ReactiveEffect對象的deps屬性中。
步驟3:當響應(yīng)式對象的屬性修改后,會觸發(fā)依賴更新,由于觸發(fā)更新的依賴列表里面存在effect方法里面創(chuàng)建的ReactiveEffect對象,所以會重新調(diào)用其run方法,在這兒也就會調(diào)用innerfunc方法。所以dummy屬性就會跟隨counter.num屬性的變化而變化
備注:上述三步驟中,提及了收集依賴和觸發(fā)依賴更新。接下來我們便看一下是如何收集依賴和觸發(fā)依賴更新的。
3.收集依賴和觸發(fā)依賴更新
(1).收集依賴
export function track(target: object, type: TrackOpTypes, key: unknown) { //是否允許收集 if (!isTracking()) { return } //對象map let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } //依賴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) } export function trackEffects( dep: Dep, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { let shouldTrack = false if (effectTrackDepth <= maxMarkerBits) { if (!newTracked(dep)) { //本一輪調(diào)用新收集的依賴 dep.n |= trackOpBit // set newly tracked //是否應(yīng)該被收集 shouldTrack = !wasTracked(dep) } } else { // Full cleanup mode. shouldTrack = !dep.has(activeEffect!) } if (shouldTrack) { //收集依賴 dep.add(activeEffect!) activeEffect!.deps.push(dep) if (__DEV__ && activeEffect!.onTrack) { activeEffect!.onTrack( Object.assign( { effect: activeEffect! }, debuggerEventExtraInfo ) ) } } }
上述代碼是收集依賴的核心代碼??催^我響應(yīng)式對象文章的話,應(yīng)該會注意到,在涉及“讀”相關(guān)操作時,就會調(diào)用track方法來收集依賴。此時就是調(diào)用的上述track方法。track方法很簡單,主要是找到對應(yīng)的依賴列表,如果沒有就創(chuàng)建一個依賴列表。
trackEffects里面先只需要關(guān)注收集依賴的邏輯,可以很明顯的看到,里面就是一個依賴的雙向添加。至于上面的那些邏輯,最主要的目的是防止重復添加依賴,我會在后面的優(yōu)化環(huán)節(jié)詳細講。
依賴模型存儲模型大致如下:
(2).觸發(fā)依賴更新
export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) { //獲取依賴map const depsMap = targetMap.get(target) 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)) { 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)) { // 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)) } } } export function triggerEffects( dep: Dep | ReactiveEffect[], debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { // spread into array for stabilization for (const effect of isArray(dep) ? dep : [...dep]) { if (effect !== activeEffect || effect.allowRecurse) { if (__DEV__ && effect.onTrigger) { effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) } if (effect.scheduler) { effect.scheduler() } else { //只需要關(guān)注這兒 effect.run() } } } }
trigger方法在響應(yīng)式對象的"寫"操作中調(diào)用,這個方法整體上只是根據(jù)不同的依賴更新類型,將依賴添加進一個依賴數(shù)組里面,最終通過triggerEffects方法更新這個依賴數(shù)組里面的依賴。
在triggerEffects方法里面,暫時只需要關(guān)注effect.run即可,此時調(diào)用的是ReactiveEffect關(guān)聯(lián)的那個回調(diào)方法,這時候也就正確的響應(yīng)式變化了。
至于effect.scheduler,我會在后續(xù)的計算屬性篇章中講到,這個方法是給計算屬性用的。
三、V3.2的響應(yīng)式優(yōu)化
上述篇幅只講述了一個整體的響應(yīng)式變化原理,接下來介紹一下V3.2帶來的響應(yīng)式性能優(yōu)化。我們先看一下Dep類型的定義
export type Dep = Set<ReactiveEffect> & TrackedMarkers /** * wasTracked and newTracked maintain the status for several levels of effect * tracking recursion. One bit per level is used to define whether the dependency * was/is tracked. */ type TrackedMarkers = { /** * wasTracked */ w: number /** * newTracked */ n: number }
可以看到,依賴列表不是一個簡簡單單的Set集合,它還存在2個用于輔助的屬性。我們創(chuàng)建依賴也是通過createDep方法,實現(xiàn)如下:
export const createDep = (effects?: ReactiveEffect[]): Dep => { const dep = new Set<ReactiveEffect>(effects) as Dep dep.w = 0 dep.n = 0 return dep }
我在這兒先說明一下這2個屬性的作用。w屬性用于判斷依賴是否已經(jīng)被收集,n屬性用于判斷依賴在本次調(diào)用中是否用到。可能現(xiàn)在還對此有疑惑,我用以下一個簡單示例來解釋。
let status = true; let dummy const depA = reactive({ num: 0 }) const depB = reactive({ num: 10 }) let innerfunc = () => { dummy = depA.num if(status){ dummy += depB.num status = false } console.log(dummy); } effect(innerfunc) depA.num = 7 depB.num = 20 //輸出為 10 7
我們來分析以下上述代碼的流程,首先調(diào)用effect方法,會執(zhí)行一次關(guān)聯(lián)的innerfunc,此時讀取了depA和depB的num屬性,所以此時ReactiveEffect對象里面的deps屬性存在2個依賴,并且輸出10。當修改depA.num屬性時,會觸發(fā)run方法,此時關(guān)注以下代碼:
if (effectTrackDepth <= maxMarkerBits) { //將當前依賴列表的所有依賴置為"已經(jīng)收集" initDepMarkers(this) } else { //不采用優(yōu)化模式,使用老流程,直接移除依賴的全部狀態(tài) cleanupEffect(this) }
因為調(diào)用effect方法時,收集過一次依賴,所以initDepMarkers方法將所有的依賴都標記為已經(jīng)收集。在run方法最后,會調(diào)用innerfunc方法。在innerfunc方法中,這一次調(diào)用中又會去收集依賴,此時關(guān)注trackEffects中的以下代碼:
if (effectTrackDepth <= maxMarkerBits) { if (!newTracked(dep)) { //本一輪調(diào)用新收集的依賴 dep.n |= trackOpBit // set newly tracked //是否應(yīng)該被收集 shouldTrack = !wasTracked(dep) } } else { // Full cleanup mode. shouldTrack = !dep.has(activeEffect!) }
在run方法中,有一個同樣的判斷effectTrackDepth <= maxMarkerBits,這個判斷是用于控制是否優(yōu)化的,后面會講為什么會存在這個判斷以及為什么maxMarkerBits的取值為30。
在這個收集邏輯中,會將本次回調(diào)中第一次使用到的依賴置為"新增依賴",我們在看innerfunc,此時只會使用到depA,不會使用到depB,因此之前存在的關(guān)于depB對象的依賴在本次調(diào)用中沒有用到。
shouldTrack屬性表示依賴是否應(yīng)該被收集,如果沒有收集,則被收集。此時innerfunc里面輸出的dummy為7。
接下來關(guān)注run里面的以下代碼:
if (effectTrackDepth <= maxMarkerBits) { //斷掉依賴關(guān)聯(lián) finalizeDepMarkers(this) } //重置位操作狀態(tài) trackOpBit = 1 << --effectTrackDepth //重置依賴收集狀態(tài) resetTracking() //棧頂出棧 effectStack.pop() const n = effectStack.length activeEffect = n > 0 ? effectStack[n - 1] : undefined
上述代碼存在于run方法里面的finally關(guān)鍵字內(nèi),當innerfunc執(zhí)行完后,里面就會執(zhí)行這里。首先便會根據(jù)判斷通過finalizeDepMarkers方法去斷掉依賴關(guān)聯(lián)。
我們看以下方法的實現(xiàn):
export const initDepMarkers = ({ deps }: ReactiveEffect) => { if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].w |= trackOpBit // set was tracked } } } export const finalizeDepMarkers = (effect: ReactiveEffect) => { const { deps } = effect if (deps.length) { let ptr = 0 for (let i = 0; i < deps.length; i++) { const dep = deps[i] if (wasTracked(dep) && !newTracked(dep)) { dep.delete(effect) } else { deps[ptr++] = dep } // clear bits dep.w &= ~trackOpBit dep.n &= ~trackOpBit } deps.length = ptr } }
這2個方法巧妙的通過位運算將調(diào)用分層。一開始將存在的依賴打上收集標簽,如果在本層中沒有使用到,則斷掉依賴關(guān)聯(lián)。當設(shè)置depB.num = 20時,首先會找到依賴列表,由于依賴列表中已經(jīng)不存在ReactiveEffect對象了,所以不會觸發(fā)依賴更新,此時不會有新的輸出。
這兒是一個優(yōu)化,斷掉不必要的關(guān)聯(lián)依賴,減少方法的調(diào)用。但我們在寫類似代碼時必須非常小心,由于斷掉了依賴關(guān)聯(lián),有可能會因為寫法不規(guī)范導致響應(yīng)失效的情況。
接下來解釋為什么要使用位運算,以及保留不走位運算的邏輯。
關(guān)注以下代碼:
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 } }
當每次觸發(fā)依賴更新時,如果都調(diào)用以上方法,會涉及大量的集合刪減操作。
我沒看過Set集合的實現(xiàn),但無非就是數(shù)組或者鏈表。如果使用數(shù)組,增刪操作會導致數(shù)組擴容或者移位,頻繁操作會耗費大量性能,如果是鏈表,也要經(jīng)過一次查找,大量的調(diào)用是會消耗性能的。
那么為什么又要保留這個方法呢,這是因為js引擎在進行整數(shù)位運算時幾乎都是按32位運算的,1 << 31后為負值,得不到預期結(jié)果,因此保留原邏輯。但其實這個邏輯幾乎不可能調(diào)到,如果真調(diào)用到這個原始邏輯,我只能說得檢查一下代碼是否規(guī)范了。
四、后話
關(guān)于響應(yīng)式就介紹到這兒,個人理解,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
vue2.0使用Sortable.js實現(xiàn)的拖拽功能示例
本篇文章主要介紹了vue2.0使用Sortable.js實現(xiàn)的拖拽功能示例,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-02-02vue2更改data里的變量不生效時,深層更改data里的變量問題
這篇文章主要介紹了vue2更改data里的變量不生效時,深層更改data里的變量問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03JavaScript實現(xiàn)簡單的圖片切換功能(實例代碼)
這篇文章主要介紹了JavaScript實現(xiàn)簡單的圖片切換功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2020-04-04Element-Ui組件 NavMenu 導航菜單的具體使用
這篇文章主要介紹了Element-Ui組件 NavMenu 導航菜單的具體使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-10-10Vue項目中使用addRoutes出現(xiàn)問題的解決方法
大家應(yīng)該都知道可以通過vue-router官方提供的一個api-->addRoutes可以實現(xiàn)路由添加的功能,事實上就也就實現(xiàn)了用戶權(quán)限,這篇文章主要給大家介紹了關(guān)于Vue項目中使用addRoutes出現(xiàn)問題的解決方法,需要的朋友可以參考下2021-08-08