在實際狀況,effect
函式內部的依賴,常因為條件分支(像是 if...else
)而發生變化,這種情況稱為「動態依賴」。
動態依賴會帶來一個問題:在某次執行中不再被使用的舊有依賴,如果沒有被處理好,會殘留在依賴列表中。
後續這個失效依賴的來源被修改時,仍然會觸發 effect
重新執行,這導致不必要的更新或邏輯錯誤。
前情回顧
- 第一次執行:
flag.value
為true
,effect
依賴flag
和name
,系統建立依賴鏈表link1(flag) -> link2(name)
。 - 觸發更新:
flag.value
變成false
,effect
重新執行。 - 第二次執行:
effect
進入else
分支,需要依賴age
。系統會複用link1(flag)
,並且幫age
建立新節點link3(age)
。在沒有清理機制時,舊的link2(name)
仍然存在在effect
的依賴鏈表中。
此時,如果修改 name.value
,因為 link2(name)
的依賴關係還在,effect
會被再次觸發,而當前 effect
的輸出內容實際只與 age
有關。
依賴清理核心思路
最直接的方法是在每次 effect
執行前,清空所有依賴再重新收集,但這樣會造成無法複用已有的鏈表節點,效能會較差。
另一個更有效率的方法是,在執行結束後,找出本次沒訪問到的節點,並只清除那一部分。
場景一:條件性依賴
感覺可以在這邊做判斷,因為 effect
從 name
切換到 age
後,depsTail
最後的位置會指向 link3
。
當執行完畢後,depsTail
指向 link3
,而 link3
存有一個 nextDep
指針,指向舊的 link2(name)
。這邊提供了一個可以判斷的依據:
「從 depsTail
指向節點的 nextDep
開始,到鏈表末尾的所有節點,都是本次執行時沒訪問到的依賴。」
以本次案例中:
depsTail
指向link3
,link3
此時仍然有nextDep
就可以清理 link3 的 nextDep
,依賴就被清理完成。
狀況二:提前返回
還記得我們上次一直觸發按鈕,鏈表上的狀態一直處於:有頭節點deps
,並且尾節點depsTail
= undefined
。
如果 effect
執行時因爲條件判斷而提前 return
,沒有訪問任何響應式資料。depsTail
會保持初始 undefined
狀態。
這邊就提供了另一個可以判斷的依據:
「當 effect
執行完畢後,如果 depsTail
是 undefined
並且 deps
頭節點存在,就說明本次執行時沒有訪問任何依賴,應該清除所有舊依賴。」
程式碼實作清除依賴
我們使用 startTrack
和 endTrack
兩個函式來管理 effect
的執行週期。
depsTail
存在,並且depsTail
的nextDep
存在,表示包含nextDep
的後續鏈表節點應該被移除,傳入clearTracking
函式。- 觸發更新完全沒讀到任何依賴(
depsTail
=undefined
,並且有sub.deps
頭節點),此時也應該要被移除,傳入clearTracking
函式。
1...2...3export class ReactiveEffect {4...5 run(){6 ...7
8 }finally{9 endTrack(this)10 activeSub = prevSub11 }12 }13 ...14}20 collapsed lines
15
16function endTrack(sub){17 const depsTail = sub.depsTail18
19 /**20 *21 * 狀況一解法: depsTail 存在,並且 depsTail 的 nextDep 存在,表示後續鏈表節點應該移除22 */23 if(depsTail){24 if(depsTail.nextDep){25 clearTracking(depsTail.nextDep)26 depsTail.nextDep = undefined27 }28 // 狀況二:depsTail 不存在,但舊的 deps 頭節點存在,清除所有節點29 }else if(sub.deps){30 clearTracking(sub.deps)31 sub.deps = undefined32 }33
34}
clearTracking 設計核心
clearTracking
函式的工作是從鏈表中移除一個 link
節點。
由於 link
節點同時存在於 dep
的訂閱者列表 (dep.subs
) 和 effect
的依賴列表 (effect.deps
) 這兩個雙向鏈表中,移除操作需要更新其在 dep.subs
列表中的 prevSub
和 nextSub
指針,然後再沿著 effect.deps
列表的 nextDep
指針繼續處理下一個待清理的節點。
clearTracking 實作
1/**2 * 清理依賴函式鏈表3 */4
5function clearTracking(link: Link){6 while(link){7 const { prevSub, nextSub, dep, nextDep} = link8
9 /**10 * 1. 如果上一個節點有 sub,那就把 nextSub 的下一個節點指向當前節點的下一個節點11 * 2. 如果沒有 sub,表示屬於頭節點,那就把 dep.subs 指向當前節點的下一個節點12 */13 if(prevSub){14 prevSub.nextSub = nextSub15 link.nextSub = undefined25 collapsed lines
16 }else{17 dep.subs = nextSub18 }19
20 /**21 * 1. 如果下一個節點有 sub,那就把 nextSub 的上一個節點指向當前節點的上一個節點22 * 2. 如果下一個節點沒有 sub,表示屬於尾節點,那就把 dep.subsTail 指向當前節點的上一個節點23 */24
25 if(nextSub){26 nextSub.prevSub = prevSub27 link.prevSub = undefined28 }else{29 dep.subsTail = prevSub30 }31
32 link.dep = link.sub = undefined33
34 link.nextDep = undefined35
36 link = nextDep37 }38}39...40...
system.ts
調整
1export function link(dep, sub){2
3 /**4 * 復用節點5 * sub.depsTail 是 undefined,並且有 sub.deps 頭節點,表示要復用6 */7 const currentDep = sub.depsTail // = link18 const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep9 // nextDep = link1.nextDep = link210 if(nextDep && nextDep.dep === dep){11 // link2.dep (name) === age ? → false! 不能復用,需要建立新 link12 sub.depsTail = nextDep13 return14 }15
26 collapsed lines
16 const newLink = {17 sub,18 dep,19 nextDep, // 讓link3的 nextDep 變成 link220 nextSub:undefined,21 prevSub:undefined22 }23
24 if(dep.subsTail){25 dep.subsTail.nextSub = newLink26 newLink.prevSub = dep.subsTail27 dep.subsTail = newLink28 }else {29 dep.subs = newLink30 dep.subsTail = newLink31 }32
33 if(sub.depsTail){34 sub.depsTail.nextDep = newLink35 sub.depsTail = newLink36 }else{37 sub.deps = newLink38 sub.depsTail = newLink39 }40
41}
重構調整:完整程式碼
system.ts
1/**2 * 依賴項3 */4interface Dep {5 // 訂閱者鏈表頭節點6 subs: Link | undefined7 // 訂閱者鏈表尾節點8 subsTail: Link | undefined9}10/**11 * 訂閱者12 */13interface Sub{150 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}33
34export function link(dep, sub){35
36 /**37 * 復用節點38 * sub.depsTail 是 undefined,並且有 sub.deps 頭節點,表示要復用39 */40 const currentDep = sub.depsTail41 const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep42 // 如果 nextDep.dep 等於我當前要收集的 dep43 if(nextDep && nextDep.dep === dep){44 sub.depsTail = nextDep // 移動指針45 return46 }47
48 const newLink = {49 sub,50 dep,51 nextDep,// 讓link3的 nextDep 變成 link252 nextSub:undefined,53 prevSub:undefined54 }55 // 將鏈表節點跟 dep 建立關聯關係56 if(dep.subsTail){57 dep.subsTail.nextSub = newLink58 newLink.prevSub = dep.subsTail59 dep.subsTail = newLink60 }else {61 dep.subs = newLink62 dep.subsTail = newLink63 }64
65 /**66 * 將鏈表節點跟 sub 建立關聯關係67 * 1.如果有尾節點,表示鏈表現在有無數個節點,在鏈表尾部新增。68 * 2.如果沒有尾節點,表示是第一次關聯鏈表,第一個節點頭尾相同。69 */70 if(sub.depsTail){71 sub.depsTail.nextDep = newLink72 sub.depsTail = newLink73 }else{74 sub.deps = newLink75 sub.depsTail = newLink76 }77
78}79
80export function propagate(subs){81 let link = subs82 let queuedEffect = []83
84 while (link){85 queuedEffect.push(link.sub)86 link = link.nextSub87 }88
89 queuedEffect.forEach(effect => effect.notify())90}91
92/**93 * 開始追蹤,將 depsTail 設為 undefined94 */95
96export function startTrack(sub){97 sub.depsTail = undefined98}99
100/**101 * 結束追蹤,找到需要清理的依賴102 */103
104export function endTrack(sub){105 const depsTail = sub.depsTail106
107 /**108 * 1. depsTail 存在,並且 depsTail 的 nextDep 存在,表示後續鏈表節點應該移除109 * 2. 觸發更新完全沒讀到任何依賴(depsTail undefined,並且有頭節點),110 * 那就把所有節點清除,否則 effect 函式會繼續被那些不相干的依賴觸發。111 */112 if(depsTail){113 if(depsTail.nextDep){114 clearTracking(depsTail.nextDep)115 depsTail.nextDep = undefined116 }117 }else if(sub.deps){118 clearTracking(sub.deps)119 sub.deps = undefined120 }121
122}123
124/**125 * 清理依賴函式鏈表126 */127
128function clearTracking(link: Link){129 while(link){130 const { prevSub, nextSub, dep, nextDep} = link131
132 /**133 * 1. 如果上一個節點有 sub,那就把 nextSub 的下一個節點指向當前節點的下一個節點134 * 2. 如果沒有 sub,表示屬於頭節點,那就把 dep.subs 指向當前節點的下一個節點135 */136 if(prevSub){// 如果我有上一個節點137 prevSub.nextSub = nextSub138 link.nextSub = undefined139 }else{// 我沒有上一個節點,我是要被刪除的頭節點140 dep.subs = nextSub141 }142
143 /**144 * 1. 如果下一個節點有 sub,那就把 nextSub 的上一個節點指向當前節點的上一個節點145 * 2. 如果下一個節點沒有 sub,表示屬於尾節點,那就把 dep.subsTail 指向當前節點的上一個節點146 */147
148 if(nextSub){// 如果我有下一個節點149 nextSub.prevSub = prevSub150 link.prevSub = undefined151 }else{ // 我沒有下一個節點,我是要被刪除的尾節點152 dep.subsTail = prevSub153 }154
155 // 清空引用156 link.dep = undefined157 link.sub = undefined158 link.nextDep = undefined159
160 // 處理下一個要移除的節點161 link = nextDep162 }163}
1import { Link, startTrack, endTrack } from './system'2
3export let activeSub;4
5export class ReactiveEffect {6
7 // 依賴項鏈表的頭節點指向 link8 deps: Link9
10 // 依賴項鏈表的尾節點指向 link11 depsTail: Link12
13 constructor(public fn){14
42 collapsed lines
15 }16
17 run(){18 const prevSub = activeSub19 activeSub = this20 startTrack(this)21
22 try{23
24 return this.fn()25
26 }finally{27 endTrack(this)28 activeSub = prevSub29 }30 }31
32 notify(){33 this.scheduler()34 }35
36 scheduler(){37 this.run()38 }39
40}41
42export function effect(fn, options){43
44 const e = new ReactiveEffect(fn)45
46 Object.assign(e, options)47
48 e.run()49
50 const runner = e.run.bind(e)51
52 runner.effect = e53
54 return runner55
56}
執行結果
失效的依賴是要實現響應式系統時需要處理的一個問題。這次我們利用 deps
鏈表和 depsTail
指標,在 effect
執行完畢後,能夠確認並移除不再使用的依賴項目。