昨天我們發現了 Effect 的問題:當 effect
被重複觸發時,它會不斷重新收集依賴,導致依賴鏈表指數級增長。
要讓 effect
記住它「訂閱過誰」,最直接的方法就是讓它自己也有一個參照列表。因此,我們分為兩大步:
effect
知道自己已經訂閱過哪些 ref
,只要 effect 知道自己訂閱過哪些依賴就可以避免新增多餘的鏈表節點,形成了一個雙向的追蹤關係。關鍵要素就是:
deps
。effect
是否是第一次收集依賴的方法。之前的步驟,剛進入頁面之後, effect
收集依賴,ref
的頭節點 subs
以及尾節點 subsTail
指向 link
,link
的 sub
指向 effect
。
我們現在要做的事是在我們現有的 Ref -> Link -> Effect
關係上,新增一條從 Effect 出發的反向依賴連結。
之前提到過一個鏈表的必要元素分別是:
如上圖,目前頁面上只有一個依賴flag.value
,我們可以讓這個鏈表的頭節點 deps
跟尾節點 depsTail
指向 link
,link
的 dep
指向依賴,我們就可以透過關係鏈找到 effect
訂閱過的依賴。
因此我們可以知道三個關鍵的角色。
effect.deps
鏈表:通過 link,記錄 effect
依賴了哪些 refeffect.depsTail
:記錄鏈表尾部,目的在可以快速增加新的鏈表節點flag.subs
鏈表:通過 link,記錄有哪些 effect
訂閱了此 refflag.subsTail
:記錄鏈表尾部,目的在可以快速增加新的鏈表節點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
export class ReactiveEffect {
// 依賴項鏈表的頭節點指向 link
deps: Link
// 依賴項鏈表的尾節點指向 link
depsTail: Link
....
}
system.ts
//system.ts
/**
* 依賴項
*/
interface Dep {
// 訂閱者鏈表頭節點
subs: Link | undefined
// 訂閱者鏈表尾節點
subsTail: Link | undefined
}
/**
* 訂閱者
*/
interface Sub{
// 訂閱者鏈表頭節點
deps: Link | undefined
// 訂閱者鏈表尾節點
depsTail: Link | undefined
}
export interface Link {
// 訂閱者
sub: Sub
// 下一個訂閱者節點
nextSub:Link
// 上一個訂閱者節點
prevSub:Link
//依賴項
dep:Dep
//下一個依賴項節點
nextDep: Link | undefined
}
接著,修改 link
函式,在建立節點時,將它加入 sub
的 deps
鏈表。
//system.ts
export function link(dep, sub){
const newLink = {
sub,
dep,// 加上依賴項
nextDep:undefined,
nextSub:undefined,
prevSub:undefined
}
...
...
/**
* 將鏈表節點跟 sub 建立關聯關係
* 1.如果有尾節點,表示鏈表現在有無數個節點,在鏈表尾部新增。
* 2.如果沒有尾節點,表示是第一次關聯鏈表,第一個節點頭尾相同。
*/
if(sub.depsTail){
sub.depsTail.nextDep = newLink
sub.depsTail = newLink
}else{
sub.deps = newLink
sub.depsTail = newLink
}
}
...
...
每次 effect 重新執行時,如何判斷是「第一次執行」還是「重新執行」?
我們可以利用頭節點deps
與尾節點depsTail
來設定三種狀態:
effect
的 dep
鏈表是沒有頭節點deps
也沒有尾節點depsTail
。depsTail
設定成undefined
。Link
。當 effect 開始重新執行時,我們將 depsTail
設為 undefined
,但保留 deps
頭節點。這樣做的目的是:
deps
鏈表仍然包含之前收集的所有依賴depsTail
會在復用過程中遍歷移動所以往後我們判斷是否是第一次依賴收集:只要有頭節點deps
,但是尾節點是undefined
,那我們就可以知道它曾經執行過。
effect.ts
run(){
const prevSub = activeSub
activeSub = this
// 開始執行,讓尾節點變 undefined
this.depsTail = undefined
...
...
}
system.ts
export function link(dep, sub){
/**
* 復用節點
* sub.depsTail 是 undefined,並且有 sub.deps 頭節點,表示要復用
*/
const currentDep = sub.depsTail
if(currentDep === undefined && sub.deps){
// 頭節點所連接的 ref 與當前要連接的 ref 相等的話
// 表示之前收集過依賴,就不收集了
if(sub.deps.dep === dep){
sub.depsTail = sub.deps //移動尾節點指針,指向剛剛復用的節點
return // 直接返回,不新增節點
}
}
...
...
}
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
透過執行順序可以更好解決這個問題,修正程式碼之後,就沒有指數觸發現象。
同步更新《嘿,日安!》技術部落格