昨天,我們知道當 effect
函式依賴多個響應式變數時,會觸發指數級更新。
觀察我們之前的做法:
run()
函式首先會將 depsTail
設為 undefined
。
// effect.ts
run(){
const prevSub = activeSub
activeSub = this
// 開始執行,讓尾節點變 undefined
this.depsTail = undefined
...
...
}
後續的依賴收集過程中,depsTail
會被賦值並指向已複用的節點。
export function link(dep, sub){
const currentDep = sub.depsTail
if(currentDep === undefined && sub.deps){
if(sub.deps.dep === dep){
sub.depsTail = sub.deps //移動尾節點指針,指向剛剛復用的節點
return
}
}
...
...
}
邏輯判斷
export function link(dep, sub){
const currentDep = sub.depsTail;
// 僅在 depsTail 為 undefined 時,才嘗試從頭節點 sub.deps 開始複用
if (currentDep === undefined && sub.deps) {
// 只會檢查依賴鏈表的第一個節點
if (sub.deps.dep === dep) {
sub.depsTail = sub.deps; // 成功後移動指針
return;
}
}
// 若不符合上述條件,則直接建立新節點...
}
深入分析後,我們了解問題在依賴節點的複用邏輯上:
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
節點上。const currentDep = sub.depsTail
if(currentDep === undefined && sub.deps){
// 依賴鏈表頭節點的 ref 與當前要連接的 ref 相等的話,表示之前收集過依賴
if(sub.deps.dep === dep){
sub.depsTail = sub.deps //移動尾節點指針,指向剛剛復用的節點
return // 直接回傳,不新增節點
}
}else if(currentDep){ //尾節點存在
// 尾節點的 nextDep 所連接的 ref,等於當前要連接的 ref
if(currentDep.nextDep?.dep === dep){
sub.depsTail = currentDep.nextDep
//移動尾節點指針,復用尾節點的 nextDep
return
}
}
目前這個結構雖然已經能正確運行,但我們可以重構得更簡潔。我們發現無論是從頭開始還是從中途繼續,我們的目標都是找到「下一個待檢查節點」。
const currentDep = sub.depsTail
// 相同邏輯:根據 currentDep 是否存在,來決定下一個要檢查的節點
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
// 如果 nextDep.dep 等於我當前要收集的 dep
if(nextDep && nextDep.dep === dep){
sub.depsTail = nextDep // 移動指針
return
}
depsTail
當前值(currentDep
= sub.depsTail
)depsTail
決定要檢查哪個節點:
depsTail
為 undefined
→ 從頭節點開始(nextDep
= sub.deps
)depsTail
有值 → 檢查下一個(nextDep
= currentDep.nextDep
)nextDep
必須存在
dep
:當前要新增鏈表節點的那個 refnextDep.dep
當前節點的下 一個節點,它所連結的 refdepsTail
到 nextDep
(記錄遍歷進度)透過將 depsTail
指針從一個單純的「尾部標記」升級為「遍歷進度指針」,我們解決了多變數依賴下的節點複用問題。
重構後的程式碼不僅修復了指數級更新的 Bug,更用統一的邏輯處理了不同情況下的節點檢查。
同步更新《嘿,日安!》技術部落格