昨天,我們知道當 effect
函式依賴多個響應式變數時,會觸發指數級更新。
觀察我們之前的做法:
-
run()
函式首先會將depsTail
設為undefined
。effect.ts 1run(){2const prevSub = activeSub3activeSub = this45// 開始執行,讓尾節點變 undefined6this.depsTail = undefined78...9...10} -
後續的依賴收集過程中,
depsTail
會被賦值並指向已複用的節點。1export function link(dep, sub){2const currentDep = sub.depsTail3if(currentDep === undefined && sub.deps){4if(sub.deps.dep === dep){5sub.depsTail = sub.deps //移動尾節點指針,指向剛剛復用的節點6return7}8}9...10...1112} -
邏輯判斷
1export function link(dep, sub){2const currentDep = sub.depsTail;3// 僅在 depsTail 為 undefined 時,才嘗試從頭節點 sub.deps 開始複用4if (currentDep === undefined && sub.deps) {5// 只會檢查依賴鏈表的第一個節點6if (sub.deps.dep === dep) {7sub.depsTail = sub.deps; // 成功後移動指針8return;9}10}1112// 若不符合上述條件,則直接建立新節點...13}
深入分析後,我們了解問題在依賴節點的複用邏輯上:
- 檢查範圍過小:複用邏輯只檢查並比對依賴鏈表的第一個節點 (
sub.deps
)。 - 指針提早退出:一旦第一個依賴(例如
flag
)複用成功,depsTail
就被賦值。導致後續依賴(例如count
)在檢查時,因爲currentDep === undefined
條件不成立,直接跳過複用檢查,盲目地建立了新的Link
節點。
核心思路
舊的邏輯中,depsTail
只是一個標記,用來判斷是否是「第一次執行複用」。現在我們要把它升級為一個「進度指針」。它的作用是標記當前複用檢查進行到了鏈表的哪個位置。
這個思考提供了一個關鍵點:「當 depsTail
存在時,代表依賴鏈表的遍歷與複用正在進行中。」
一、實作:擴充檢查邏輯
因此,我們就可以在原有的 link
函式中,增加一個額外的檢查邏輯。
當第一個 if
條件不成立,但 depsTail
(currentDep
) 確實存在時,就意味著我們不應該從頭節點開始檢查,而應該從當前 depsTail
所在節點的下一個節點 (currentDep.nextDep
) 繼續檢查。
依照上面的執行邏輯,flag
複用成功後,depsTail
指向 Link1
。我們需要新增的邏輯,就是要從 Link1
的 nextDep
,也就是 Link2
,繼續進行檢查。
檢查邏輯的核心:如果尾節點 (depsTail
) 存在,並且這個尾節點還有下一個節點 (nextDep
),我們就應該檢查這個 nextDep
是否是我們要找的目標,如果是,就直接複用它。
- 確認狀態:檢查
depsTail
是否有值。如果有,代表開始復用,並且停在鏈表的某個節點上(像是Link1
)。 - 尋找下個節點:我們的下一個目標,自然就是當前
depsTail
所指向節點的「下一個節點」,也就是currentDep.nextDep
(對應到我們的例子,就是Link2
)。 - 進行比對:我們需要判斷這個節點所連接的依賴 (
currentDep.nextDep.dep
),是否就是我們當前正要處理的依賴 (count
)。 - 執行複用:如果比對成功,就將
depsTail
這個「進度指針」向前移動到這個nextDep
節點上。
1const currentDep = sub.depsTail2 if(currentDep === undefined && sub.deps){3
4 // 依賴鏈表頭節點的 ref 與當前要連接的 ref 相等的話,表示之前收集過依賴5 if(sub.deps.dep === dep){6 sub.depsTail = sub.deps //移動尾節點指針,指向剛剛復用的節點7 return // 直接回傳,不新增節點8 }9 }else if(currentDep){ //尾節點存在10
11 // 尾節點的 nextDep 所連接的 ref,等於當前要連接的 ref12 if(currentDep.nextDep?.dep === dep){13 sub.depsTail = currentDep.nextDep14 //移動尾節點指針,復用尾節點的 nextDep15 return2 collapsed lines
16 }17 }
二、重構與簡化
目前這個結構雖然已經能正確運行,但我們可以重構得更簡潔。我們發現無論是從頭開始還是從中途繼續,我們的目標都是找到「下一個待檢查節點」。
1const currentDep = sub.depsTail2// 相同邏輯:根據 currentDep 是否存在,來決定下一個要檢查的節點3const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep4// 如果 nextDep.dep 等於我當前要收集的 dep5if(nextDep && nextDep.dep === dep){6 sub.depsTail = nextDep // 移動指針7 return8}
完整執行流程
- 取得
depsTail
當前值(currentDep
=sub.depsTail
) - 根據
depsTail
決定要檢查哪個節點:- 若
depsTail
為undefined
→ 從頭節點開始(nextDep
=sub.deps
) - 若
depsTail
有值 → 檢查下一個(nextDep
=currentDep.nextDep
)
- 若
- 檢查是否可以復用:
nextDep
必須存在dep
:當前要新增鏈表節點的那個 refnextDep.dep
當前節點的下 一個節點,它所連結的 ref
- 如果可以復用:
- 移動
depsTail
到nextDep
(記錄遍歷進度) - 不建立新的 Link,提前返回
- 移動
透過將 depsTail
指針從一個單純的「尾部標記」升級為「遍歷進度指針」,我們解決了多變數依賴下的節點複用問題。
重構後的程式碼不僅修復了指數級更新的 Bug,更用統一的邏輯處理了不同情況下的節點檢查。