嘿,日安!

Day 14 - Effect:清理依賴的場景

23rd September 2025
Front-end
Vue3
IT鐵人賽2025
從零到一打造 Vue3 響應式系統
Last updated:23rd September 2025
7 Minutes
1243 Words

在解決了鏈表節點指數增長的問題後,我們還需要注意依賴的有效性。

effect 的執行路徑可能因為條件判斷或程式邏輯不同而改變,導致這次執行中不再需要某些依賴。

如果這些過期依賴沒有被清理:

  • 會造成 記憶體洩漏:不需要的鏈表節點一直被保留。
  • 會導致 不必要的更新:effect 雖然已經不依賴某個 ref,但這個 ref 的變化仍然會觸發 effect。
  • 會引發 效能下降:隨著時間累積,無效鏈表節點越來越多,增加整體執行成本。

因此,在收集依賴時,不只要能正確複用,也必須具備 清理過期依賴 的機制。

下面我們來說需要清理依賴的兩個狀況。

場景一:條件型依賴

1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
<meta charset="UTF-8" />
5
<title>Document</title>
6
<style>
7
body {
8
padding: 150px;
9
}
10
</style>
11
</head>
12
<body>
13
<div id="app"></div>
14
<button id="flagBtn">update flag</button>
15
<button id="nameBtn">update name</button>
31 collapsed lines
16
<button id="ageBtn">update age</button>
17
<script type="module">
18
// import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
19
import { ref, effect } from '../dist/reactivity.esm.js'
20
21
const flag = ref(true)
22
const name = ref('姓名')
23
const age = ref(18)
24
25
effect(() => {
26
console.count('effect')
27
28
if(flag.value){
29
app.innerHTML = name.value
30
}else{
31
app.innerHTML = age.value
32
}
33
})
34
35
flagBtn.onclick = () => {
36
flag.value = !flag.value
37
}
38
nameBtn.onclick = () => {
39
name.value = '姓名' + Math.random()
40
}
41
ageBtn.onclick = () => {
42
age.value++
43
}
44
</script>
45
</body>
46
</html>

我們現在有三個變數,flagnameage

  • 如果 flag 是 true,那點擊 name會觸發更新,但是點擊 age 不會觸發更新。
  • 如果 flag 是 false,那點擊 age 會觸發更新,但是點擊 name 不會觸發更新。

這很合理,因為沒有執行的狀況就不應該觸發。

實際情況

default

  • 初始化:輸出 effect: 1flag是 true。
  • 點擊 update flag:effect 更新,輸出 effect: 2flag是 false。
  • 點擊 update name:我們期望他沒有反應,因為「如果 flag 是 false,點擊 name 不會觸發更新。」
  • 但 effect 仍然更新,console 輸出 effect: 3

問題說明

我們先來看一下 effect 以及 name 裡面的內容,執行步驟是初始化後先點擊 update flag,再點擊 update name

檢查 Effect 回傳值

1
const e = effect(() => {
2
console.count('effect')
3
if(flag.value){
4
app.innerHTML = name.value
5
}else{
6
app.innerHTML = age.value
7
}
8
})
9
...
10
...
11
nameBtn.onclick = () => {
12
name.value = '姓名' + Math.random()
13
console.dir(e)
14
console.log(name)
15
}

default

我們預期 effect 回傳值中,不會有 name節點:實際上是正確的,目前 Link 節點是 flagage

default

我們預期 name 回傳值中,不會有頭節點跟尾節點,但實際上輸出結果是自己是正在被訂閱的狀態。

異常情況

我們現在了解異常情況是

  • effect 裡面訂閱 flagage 沒有 name,是我們要的正確結果。
  • name 裡面發現自己被 effect 訂閱,但鏈表上沒有它節點,它應該要被清除。

Effect 依賴清理圖解問題說明

頁面初始化

default

遇到 flag 收集依賴,建立鏈表節點。

default

遇到 name,建立鏈表節點。

點擊按鈕,觸發更新

default

點擊按鈕,effect 重新執行,run 函式讓depsTail 指向undefined

default

接著link函式建立鏈表節點,先檢查是否可以復用,頭節點desp存在depsTail == undefined,可以復用。

default

發現可以復用,depsTail 指向 link1,此時 flag 是 false。

default

由於 flag 是 false,接下來遇到 age,之前沒有收集依賴過,於是建立新的鏈表節點。

default

建立age鏈表節點之後,depsTailnextDep 以及 depsTail 指向新節點。

default

此時你看黃底色的地方,發現 effect 已經沒有存 link2,但是 link2 的 sub 仍然指向 effect。

所以才會出現 name 更新,仍然會讓 effect 執行的情況。

場景二:提前返回

1
const flag = ref(true)
2
const name = ref('姓名')
3
const age = ref(18)
4
let count = 0
5
6
effect(() => {
7
console.count('effect')
8
9
if(count>0) return
10
count++
11
12
if(flag.value){
13
app.innerHTML = name.value
14
}else{
15
app.innerHTML = age.value
12 collapsed lines
16
}
17
})
18
19
flagBtn.onclick = () => {
20
flag.value = !flag.value
21
}
22
nameBtn.onclick = () => {
23
name.value = '姓名' + Math.random()
24
}
25
ageBtn.onclick = () => {
26
age.value++
27
}

到時候修正會一起調整,所以我們先來說第二種需要清除依賴的狀況。

預期:只會觸發兩次。(因為 count>0 會返回,無論如何點擊按鈕就不再觸發)

初始化

  • 此時count0,繼續往下執行
    • count++count現在是1。
    • 讀到 flag.value → 收集依賴:flag
    • flagtrue,接著讀到 name.value → 收集依賴:name

此時 Link 節點是 flagnamedepsTail 指向 name。 effect 被 flagname 兩個 ref 訂閱。

default

  • 點擊 name 按鈕:
    • 觸發 effect 重新 run()
    • depsTail = undefined
    • 進入 effect 函式,馬上遇到 if (count > 0) return被中斷,因此鏈表節點上沒有任何改動。

遇到 return 程式應該會中斷,可是你一直點擊 name按鈕,console.count('effect') 仍然一直觸發更新。

default

但遇到 if (count > 0) return,effect 沒有存取任何依賴, deps 鏈表上應該要是空的,可是鏈表上面有之前建立的鏈表沒有清除。

此時鏈表上的狀態一直處於:有頭節點deps,並且尾節點depsTail = undefined

明天我們會來談怎麼清除這些依賴。

Article title:Day 14 - Effect:清理依賴的場景
Article author:日安
Release time:23rd September 2025