昨天我們發現了 Effect 的問題:當 effect
被重複觸發時,它會不斷重新收集依賴,導致依賴鏈表指數級增長。
要讓 effect
記住它「訂閱過誰」,最直接的方法就是讓它自己也有一個參照列表。因此,我們分為兩大步:
- 建立反向依賴鏈表:建立一個新的鏈表,讓
effect
知道自己已經訂閱過哪些ref
,只要 effect 知道自己訂閱過哪些依賴就可以避免新增多餘的鏈表節點,形成了一個雙向的追蹤關係。 - 實現節點複用機制:下次再次觸發更新之後,就可以藉由查找訂閱過的依賴判斷。如果第一次執行收集過依賴,重復使用之前的鏈表節點,不建立新的節點。如果沒有收集過,就建立一個全新的鏈表節點。
關鍵要素就是:
- 需要建立一個新的鏈表讓 effect 紀錄曾經收集過的依賴,這個鏈表我們叫
deps
。 - 需要一個判斷
effect
是否是第一次收集依賴的方法。
初始化頁面
之前的步驟,剛進入頁面之後, effect
收集依賴,ref
的頭節點 subs
以及尾節點 subsTail
指向 link
,link
的 sub
指向 effect
。
步驟一:建立反向依賴鏈表
我們現在要做的事是在我們現有的 Ref -> Link -> Effect
關係上,新增一條從 Effect 出發的反向依賴連結。
之前提到過一個鏈表的必要元素分別是:
- 頭節點
- 尾節點
- 彼此建立的關聯
如上圖,目前頁面上只有一個依賴flag.value
,我們可以讓這個鏈表的頭節點 deps
跟尾節點 depsTail
指向 link
,link
的 dep
指向依賴,我們就可以透過關係鏈找到 effect
訂閱過的依賴。
因此我們可以知道三個關鍵的角色。
三個關鍵角色
Effect
effect.deps
鏈表:通過 link,記錄effect
依賴了哪些 refeffect.depsTail
:記錄鏈表尾部,目的在可以快速增加新的鏈表節點
Ref(flag)
flag.subs
鏈表:通過 link,記錄有哪些effect
訂閱了此 refflag.subsTail
:記錄鏈表尾部,目的在可以快速增加新的鏈表節點
Link:雙向橋樑節點
Link 是連接 Effect
和 Ref
的橋樑,同時存在於兩個鏈表中。
核心屬性:
link.sub
:指向發起的訂閱者 (effect
)link.dep
:指向被訂閱的ref
在 Effect 鏈表中的位置:
link.nextDep/prevDep
:指向effect.deps
鏈表的下/上一個節點
在 Ref 鏈表中的位置:
link.nextSub/prevSub
:指向ref.subs
鏈表的下/上一個節點
透過上面的方法,我們可以知道三件事:
- 雙向查詢:通過
Link
可以找到effect
和ref
- 雙鏈表成員:
Link
同時是兩個鏈表的成員- 是
effect.deps
鏈表的一個節點 - 是
ref.subs
鏈表的一個節點
- 是
- 關係管理:一個
Link
代表一個訂閱關係
首先我們更新 effect.ts
和 system.ts
來實作這個新的資料結構。
定義型別
effect.ts
1export class ReactiveEffect {2
3 // 依賴項鏈表的頭節點指向 link4 deps: Link5 // 依賴項鏈表的尾節點指向 link6 depsTail: Link7
8 ....9
10}
system.ts
1/**2 * 依賴項3 */4interface Dep {5 // 訂閱者鏈表頭節點6 subs: Link | undefined7 // 訂閱者鏈表尾節點8 subsTail: Link | undefined9}10/**11 * 訂閱者12 */13interface Sub{19 collapsed lines
14 // 訂閱者鏈表頭節點15 deps: Link | undefined16 // 訂閱者鏈表尾節點17 depsTail: Link | undefined18}19
20export interface Link {21 // 訂閱者22 sub: Sub23 // 下一個訂閱者節點24 nextSub:Link25 // 上一個訂閱者節點26 prevSub:Link27 //依賴項28 dep:Dep29
30 //下一個依賴項節點31 nextDep: Link | undefined32}
增加 link 判斷
接著,修改 link
函式,在建立節點時,將它加入 sub
的 deps
鏈表。
1export function link(dep, sub){2
3 const newLink = {4 sub,5 dep,// 加上依賴項6 nextDep:undefined,7 nextSub:undefined,8 prevSub:undefined9 }10 ...11 ...12
13 /**14 * 將鏈表節點跟 sub 建立關聯關係15 collapsed lines
15 * 1.如果有尾節點,表示鏈表現在有無數個節點,在鏈表尾部新增。16 * 2.如果沒有尾節點,表示是第一次關聯鏈表,第一個節點頭尾相同。17 */18 if(sub.depsTail){19 sub.depsTail.nextDep = newLink20 sub.depsTail = newLink21 }else{22 sub.deps = newLink23 sub.depsTail = newLink24 }25
26}27
28...29...
步驟二:實現節點複用機制
每次 effect 重新執行時,如何判斷是「第一次執行」還是「重新執行」?
我們可以利用頭節點deps
與尾節點depsTail
來設定三種狀態:
- 初始狀態:當從未執行過收集依賴:
effect
的dep
鏈表是沒有頭節點deps
也沒有尾節點depsTail
。 - 執行時:正在重新執行中,需要復用節點:將尾節點
depsTail
設定成undefined
。 - 執行完成:鏈表更新完成:頭尾節點都是
Link
。
當 effect 開始重新執行時,我們將 depsTail
設為 undefined
,但保留 deps
頭節點。這樣做的目的是:
- 標記重新執行的狀態,讓 link 函式可以知道需要復用節點
deps
鏈表仍然包含之前收集的所有依賴depsTail
會在復用過程中遍歷移動
所以往後我們判斷是否是第一次依賴收集:只要有頭節點deps
,但是尾節點是undefined
,那我們就可以知道它曾經執行過。
實作 effect.ts
1run(){2 const prevSub = activeSub3 activeSub = this4
5 // 開始執行,讓尾節點變 undefined6 this.depsTail = undefined7
8 ...9 ...10 }
實作 system.ts
1export function link(dep, sub){2
3/**4 * 復用節點5 * sub.depsTail 是 undefined,並且有 sub.deps 頭節點,表示要復用6 */7 const currentDep = sub.depsTail8 if(currentDep === undefined && sub.deps){9 // 頭節點所連接的 ref 與當前要連接的 ref 相等的話10 // 表示之前收集過依賴,就不收集了11 if(sub.deps.dep === dep){12 sub.depsTail = sub.deps //移動尾節點指針,指向剛剛復用的節點13 return // 直接返回,不新增節點14 }15 }4 collapsed lines
16 ...17 ...18
19}
完整執行流程
第一次執行
- effect 初始化:
deps = undefined
,depsTail = undefined
- 執行
run()
:depsTail = undefined
- 讀取
ref.value
link()
開始判斷:沒有deps
→ 建立新鏈表節點- 執行結束:
deps = Link1
,depsTail = Link1
第二次執行(點擊按鈕)
- 執行前:
deps = Link1
,depsTail = Link1
- 執行
run()
:depsTail = undefined
- 讀取
ref.value
link()
開始判斷:- 條件:
depsTail = undefined
、deps
存在 、deps.dep === 當前 dep
- 設定好
depsTail
尾節點,return
不建立新的節點。
- 條件:
- 執行結束:
deps = Link1
,depsTail = Link1
透過執行順序可以更好解決這個問題,修正程式碼之後,就沒有指數觸發現象。