嘿,日安!

Day 25 - Watch :清理 SideEffect

4th October 2025
Front-end
Vue3
IT鐵人賽2025
從零到一打造 Vue3 響應式系統
Last updated:9th October 2025
4 Minutes
761 Words

watch 的一個核心用途是在響應式資料發生改變時,執行 Side Effect。

然而,當 Side Effect 是非同步或需要手動清理時,就會出現一個常見的問題:如果監聽的資料在短時間內多次變更,前一次的 Side Effect可能沒清理乾淨,就會與下一次的 Side Effect 產生衝突或造成資源洩漏。

1
<!DOCTYPE html>
2
<html lang="en">
3
4
<head>
5
<meta charset="UTF-8" />
6
<title>Document</title>
7
<style>
8
#app,#div{
9
width: 100px;
10
height: 100px;
11
background-color: red;
12
margin-bottom: 10px;
13
}
14
#div{
15
background-color: blue;
34 collapsed lines
16
}
17
</style>
18
</head>
19
20
<body>
21
<div id="app"></div>
22
<div id="div"></div>
23
<button id="button">按鈕</button>
24
<script type="module">
25
// import { ref, watch, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
26
import { ref, watch } from '../dist/reactivity.esm.js'
27
28
const flag = ref(true)
29
30
watch(flag, (newVal, oldVal) => {
31
const dom = newVal ? app : div
32
33
function handler () {
34
console.log(newVal ? '點擊app' : '點擊div')
35
}
36
37
dom.addEventListener('click', handler)
38
},
39
{ immediate: true }
40
)
41
42
button.onclick = () => {
43
flag.value = !flag.value
44
}
45
46
</script>
47
</body>
48
49
</html>

上述範例是一個常見的資源洩漏問題:現在可以看到有兩個色塊,點了app 會被觸發,點擊 div 沒有反應,點擊按鈕,當 flagtrue 變為 false 時,此時div 點擊後會被觸發,但你點擊 app 控制台仍然有輸出,這是因為app 元素上註冊的 click 事件監聽器並沒有被移除。

因此,即使邏輯上它不應該再響應點擊,但 click 監聽器依然殘留在記憶體中並繼續觸發。

官方的解決方案是,有一個onCleanup函式:

1
<body>
2
<div id="app"></div>
3
<div id="div"></div>
4
<button id="button">按鈕</button>
5
<script type="module">
6
import { ref, watch, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
7
// import { ref, watch } from '../dist/reactivity.esm.js'
8
9
const flag = ref(true)
10
11
watch(flag, (newVal, oldVal, onCleanup) => {
12
const dom = newVal ? app : div
13
14
function handler () {
15
console.log(newVal ? '點擊app' : '點擊div')
17 collapsed lines
16
}
17
18
dom.addEventListener('click', handler)
19
20
onCleanup(() => {
21
dom.removeEventListener('click', handler)
22
})
23
},
24
{ immediate: true }
25
)
26
27
button.onclick = () => {
28
flag.value = !flag.value
29
}
30
31
</script>
32
</body>

我們現在在監聽的時候綁一個監聽事件,onCleanup 接受一個 callback 函式,我們在函式中寫移除事件。

onCleanup 註冊的 callback 函式會在下一次 watch callback 即將執行**之前被呼叫,**這個時機確保了我們在新的副作用出現前,清理掉上一個過期的 Side Effect。

default

你會發現回去點 app,它不會再被觸發了。

1
export function watch(source, cb, options) {
2
...
3
...
4
let cleanup = null
5
6
function onCleanup(cb) {
7
cleanup = cb
8
}
9
10
function job() {
11
12
if(cleanup) {
13
// 確認是不是要清理之前的 sideEffect 函式
14
cleanup()
15
cleanup = null
12 collapsed lines
16
}
17
18
// 執行 effect 的函式,得到新的數值,不能直接執行 getter,因為要收集依賴
19
const newValue = effect.run()
20
21
cb(newValue, oldValue, onCleanup)
22
23
oldValue = newValue
24
}
25
...
26
...
27
}

我們儲存外部傳入 onCleanup 的 callback 函式,把它儲存到變數之中,接著判斷,如果 onCleanup 傳入函式存在,在每次執行 job 函式之前,先執行一次清理函式。

onCleanupwatch API 中一個很重要但容易被忽略的特性。它為開發者提供了一個標準化的機制,來應對 Side Effect 帶來的挑戰:資源洩漏(如未移除的事件監聽器)與非同步競爭條件(如過期的網路請求)。

Article title:Day 25 - Watch :清理 SideEffect
Article author:日安
Release time:4th October 2025