嘿,日安!

Day 15 - Effect:依賴清理實作方案

24th September 2025
Front-end
Vue3
IT鐵人賽2025
從零到一打造 Vue3 響應式系統
Last updated:24th September 2025
11 Minutes
2156 Words

在實際狀況,effect 函式內部的依賴,常因為條件分支(像是 if...else)而發生變化,這種情況稱為「動態依賴」。

動態依賴會帶來一個問題:在某次執行中不再被使用的舊有依賴,如果沒有被處理好,會殘留在依賴列表中。

後續這個失效依賴的來源被修改時,仍然會觸發 effect 重新執行,這導致不必要的更新或邏輯錯誤。

前情回顧

default

  • 第一次執行flag.valuetrueeffect 依賴 flagname,系統建立依賴鏈表 link1(flag) -> link2(name)
  • 觸發更新flag.value 變成 falseeffect 重新執行。
  • 第二次執行effect 進入 else 分支,需要依賴 age。系統會複用 link1(flag),並且幫 age 建立新節點 link3(age)。在沒有清理機制時,舊的 link2(name) 仍然存在在 effect 的依賴鏈表中。

此時,如果修改 name.value,因為 link2(name) 的依賴關係還在,effect 會被再次觸發,而當前 effect 的輸出內容實際只與 age 有關。

依賴清理核心思路

最直接的方法是在每次 effect 執行前,清空所有依賴再重新收集,但這樣會造成無法複用已有的鏈表節點,效能會較差。

另一個更有效率的方法是,在執行結束後,找出本次沒訪問到的節點,並只清除那一部分。

場景一:條件性依賴

感覺可以在這邊做判斷,因為 effectname 切換到 age 後,depsTail 最後的位置會指向 link3

default

當執行完畢後,depsTail 指向 link3,而 link3 存有一個 nextDep 指針,指向舊的 link2(name)。這邊提供了一個可以判斷的依據:

「從 depsTail 指向節點的 nextDep 開始,到鏈表末尾的所有節點,都是本次執行時沒訪問到的依賴。」

以本次案例中:

  1. depsTail 指向link3
  2. link3 此時仍然有 nextDep

就可以清理 link3 的 nextDep,依賴就被清理完成。

狀況二:提前返回

default

還記得我們上次一直觸發按鈕,鏈表上的狀態一直處於:有頭節點deps,並且尾節點depsTail = undefined

如果 effect 執行時因爲條件判斷而提前 return,沒有訪問任何響應式資料。depsTail 會保持初始 undefined 狀態。

這邊就提供了另一個可以判斷的依據:

「當 effect 執行完畢後,如果 depsTailundefined 並且 deps 頭節點存在,就說明本次執行時沒有訪問任何依賴,應該清除所有舊依賴。」

程式碼實作清除依賴

我們使用 startTrackendTrack 兩個函式來管理 effect 的執行週期。

  1. depsTail 存在,並且 depsTailnextDep 存在,表示包含nextDep的後續鏈表節點應該被移除,傳入clearTracking函式。
  2. 觸發更新完全沒讀到任何依賴(depsTail = undefined,並且有sub.deps頭節點),此時也應該要被移除,傳入clearTracking函式。
effect.ts
1
...
2
...
3
export class ReactiveEffect {
4
...
5
run(){
6
...
7
8
}finally{
9
endTrack(this)
10
activeSub = prevSub
11
}
12
}
13
...
14
}
20 collapsed lines
15
16
function endTrack(sub){
17
const depsTail = sub.depsTail
18
19
/**
20
*
21
* 狀況一解法: depsTail 存在,並且 depsTail 的 nextDep 存在,表示後續鏈表節點應該移除
22
*/
23
if(depsTail){
24
if(depsTail.nextDep){
25
clearTracking(depsTail.nextDep)
26
depsTail.nextDep = undefined
27
}
28
// 狀況二:depsTail 不存在,但舊的 deps 頭節點存在,清除所有節點
29
}else if(sub.deps){
30
clearTracking(sub.deps)
31
sub.deps = undefined
32
}
33
34
}

clearTracking 設計核心

default

clearTracking 函式的工作是從鏈表中移除一個 link 節點。

default

由於 link 節點同時存在於 dep 的訂閱者列表 (dep.subs) 和 effect 的依賴列表 (effect.deps) 這兩個雙向鏈表中,移除操作需要更新其在 dep.subs 列表中的 prevSubnextSub 指針,然後再沿著 effect.deps 列表的 nextDep 指針繼續處理下一個待清理的節點。

clearTracking 實作

1
/**
2
* 清理依賴函式鏈表
3
*/
4
5
function clearTracking(link: Link){
6
while(link){
7
const { prevSub, nextSub, dep, nextDep} = link
8
9
/**
10
* 1. 如果上一個節點有 sub,那就把 nextSub 的下一個節點指向當前節點的下一個節點
11
* 2. 如果沒有 sub,表示屬於頭節點,那就把 dep.subs 指向當前節點的下一個節點
12
*/
13
if(prevSub){
14
prevSub.nextSub = nextSub
15
link.nextSub = undefined
25 collapsed lines
16
}else{
17
dep.subs = nextSub
18
}
19
20
/**
21
* 1. 如果下一個節點有 sub,那就把 nextSub 的上一個節點指向當前節點的上一個節點
22
* 2. 如果下一個節點沒有 sub,表示屬於尾節點,那就把 dep.subsTail 指向當前節點的上一個節點
23
*/
24
25
if(nextSub){
26
nextSub.prevSub = prevSub
27
link.prevSub = undefined
28
}else{
29
dep.subsTail = prevSub
30
}
31
32
link.dep = link.sub = undefined
33
34
link.nextDep = undefined
35
36
link = nextDep
37
}
38
}
39
...
40
...

system.ts 調整

1
export function link(dep, sub){
2
3
/**
4
* 復用節點
5
* sub.depsTail 是 undefined,並且有 sub.deps 頭節點,表示要復用
6
*/
7
const currentDep = sub.depsTail // = link1
8
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
9
// nextDep = link1.nextDep = link2
10
if(nextDep && nextDep.dep === dep){
11
// link2.dep (name) === age ? → false! 不能復用,需要建立新 link
12
sub.depsTail = nextDep
13
return
14
}
15
26 collapsed lines
16
const newLink = {
17
sub,
18
dep,
19
nextDep, // 讓link3的 nextDep 變成 link2
20
nextSub:undefined,
21
prevSub:undefined
22
}
23
24
if(dep.subsTail){
25
dep.subsTail.nextSub = newLink
26
newLink.prevSub = dep.subsTail
27
dep.subsTail = newLink
28
}else {
29
dep.subs = newLink
30
dep.subsTail = newLink
31
}
32
33
if(sub.depsTail){
34
sub.depsTail.nextDep = newLink
35
sub.depsTail = newLink
36
}else{
37
sub.deps = newLink
38
sub.depsTail = newLink
39
}
40
41
}

重構調整:完整程式碼

system.ts

system.ts
1
/**
2
* 依賴項
3
*/
4
interface Dep {
5
// 訂閱者鏈表頭節點
6
subs: Link | undefined
7
// 訂閱者鏈表尾節點
8
subsTail: Link | undefined
9
}
10
/**
11
* 訂閱者
12
*/
13
interface Sub{
150 collapsed lines
14
// 訂閱者鏈表頭節點
15
deps: Link | undefined
16
// 訂閱者鏈表尾節點
17
depsTail: Link | undefined
18
}
19
20
export interface Link {
21
// 訂閱者
22
sub: Sub
23
// 下一個訂閱者節點
24
nextSub:Link
25
// 上一個訂閱者節點
26
prevSub:Link
27
//依賴項
28
dep:Dep
29
30
//下一個依賴項節點
31
nextDep: Link | undefined
32
}
33
34
export function link(dep, sub){
35
36
/**
37
* 復用節點
38
* sub.depsTail 是 undefined,並且有 sub.deps 頭節點,表示要復用
39
*/
40
const currentDep = sub.depsTail
41
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
42
// 如果 nextDep.dep 等於我當前要收集的 dep
43
if(nextDep && nextDep.dep === dep){
44
sub.depsTail = nextDep // 移動指針
45
return
46
}
47
48
const newLink = {
49
sub,
50
dep,
51
nextDep,// 讓link3的 nextDep 變成 link2
52
nextSub:undefined,
53
prevSub:undefined
54
}
55
// 將鏈表節點跟 dep 建立關聯關係
56
if(dep.subsTail){
57
dep.subsTail.nextSub = newLink
58
newLink.prevSub = dep.subsTail
59
dep.subsTail = newLink
60
}else {
61
dep.subs = newLink
62
dep.subsTail = newLink
63
}
64
65
/**
66
* 將鏈表節點跟 sub 建立關聯關係
67
* 1.如果有尾節點,表示鏈表現在有無數個節點,在鏈表尾部新增。
68
* 2.如果沒有尾節點,表示是第一次關聯鏈表,第一個節點頭尾相同。
69
*/
70
if(sub.depsTail){
71
sub.depsTail.nextDep = newLink
72
sub.depsTail = newLink
73
}else{
74
sub.deps = newLink
75
sub.depsTail = newLink
76
}
77
78
}
79
80
export function propagate(subs){
81
let link = subs
82
let queuedEffect = []
83
84
while (link){
85
queuedEffect.push(link.sub)
86
link = link.nextSub
87
}
88
89
queuedEffect.forEach(effect => effect.notify())
90
}
91
92
/**
93
* 開始追蹤,將 depsTail 設為 undefined
94
*/
95
96
export function startTrack(sub){
97
sub.depsTail = undefined
98
}
99
100
/**
101
* 結束追蹤,找到需要清理的依賴
102
*/
103
104
export function endTrack(sub){
105
const depsTail = sub.depsTail
106
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 = undefined
116
}
117
}else if(sub.deps){
118
clearTracking(sub.deps)
119
sub.deps = undefined
120
}
121
122
}
123
124
/**
125
* 清理依賴函式鏈表
126
*/
127
128
function clearTracking(link: Link){
129
while(link){
130
const { prevSub, nextSub, dep, nextDep} = link
131
132
/**
133
* 1. 如果上一個節點有 sub,那就把 nextSub 的下一個節點指向當前節點的下一個節點
134
* 2. 如果沒有 sub,表示屬於頭節點,那就把 dep.subs 指向當前節點的下一個節點
135
*/
136
if(prevSub){// 如果我有上一個節點
137
prevSub.nextSub = nextSub
138
link.nextSub = undefined
139
}else{// 我沒有上一個節點,我是要被刪除的頭節點
140
dep.subs = nextSub
141
}
142
143
/**
144
* 1. 如果下一個節點有 sub,那就把 nextSub 的上一個節點指向當前節點的上一個節點
145
* 2. 如果下一個節點沒有 sub,表示屬於尾節點,那就把 dep.subsTail 指向當前節點的上一個節點
146
*/
147
148
if(nextSub){// 如果我有下一個節點
149
nextSub.prevSub = prevSub
150
link.prevSub = undefined
151
}else{ // 我沒有下一個節點,我是要被刪除的尾節點
152
dep.subsTail = prevSub
153
}
154
155
// 清空引用
156
link.dep = undefined
157
link.sub = undefined
158
link.nextDep = undefined
159
160
// 處理下一個要移除的節點
161
link = nextDep
162
}
163
}

effect.ts
1
import { Link, startTrack, endTrack } from './system'
2
3
export let activeSub;
4
5
export class ReactiveEffect {
6
7
// 依賴項鏈表的頭節點指向 link
8
deps: Link
9
10
// 依賴項鏈表的尾節點指向 link
11
depsTail: Link
12
13
constructor(public fn){
14
42 collapsed lines
15
}
16
17
run(){
18
const prevSub = activeSub
19
activeSub = this
20
startTrack(this)
21
22
try{
23
24
return this.fn()
25
26
}finally{
27
endTrack(this)
28
activeSub = prevSub
29
}
30
}
31
32
notify(){
33
this.scheduler()
34
}
35
36
scheduler(){
37
this.run()
38
}
39
40
}
41
42
export 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 = e
53
54
return runner
55
56
}

執行結果

default

失效的依賴是要實現響應式系統時需要處理的一個問題。這次我們利用 deps 鏈表和 depsTail 指標,在 effect 執行完畢後,能夠確認並移除不再使用的依賴項目。

Article title:Day 15 - Effect:依賴清理實作方案
Article author:日安
Release time:24th September 2025