watch
的一個核心用途是在響應式資料發生改變時,執行 Side Effect。
然而,當 Side Effect 是非同步或需要手動清理時,就會出現一個常見的問題:如果監聽的資料在短時間內多次變更,前一次的 Side Effect可能沒清理乾淨,就會與下一次的 Side Effect 產生衝突或造成資源洩漏。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
<style>
#app,#div{
width: 100px;
height: 100px;
background-color: red;
margin-bottom: 10px;
}
#div{
background-color: blue;
}
</style>
</head>
<body>
<div id="app"></div>
<div id="div"></div>
<button id="button">按鈕</button>
<script type="module">
// import { ref, watch, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { ref, watch } from '../dist/reactivity.esm.js'
const flag = ref(true)
watch(flag, (newVal, oldVal) => {
const dom = newVal ? app : div
function handler () {
console.log(newVal ? '點擊app' : '點擊div')
}
dom.addEventListener('click', handler)
},
{ immediate: true }
)
button.onclick = () => {
flag.value = !flag.value
}
</script>
</body>
</html>
上述範例是一個常見的資源洩漏問題:現在可以看到有兩個色塊,點了app
會被觸發,點擊 div
沒有反應,點擊按鈕,當 flag
從 true
變為 false
時,此時div
點擊後會被觸發,但你點擊 app
控制台仍然有輸出,這是因為app
元素上註冊的 click
事件監聽器並沒有被移除。
因此,即使邏輯上它不應該再響應點擊,但 click 監聽器依然殘留在記憶體中並繼續觸發。
官方的解決方案是,有一個onCleanup
函式:
<body>
<div id="app"></div>
<div id="div"></div>
<button id="button">按鈕</button>
<script type="module">
import { ref, watch, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
// import { ref, watch } from '../dist/reactivity.esm.js'
const flag = ref(true)
watch(flag, (newVal, oldVal, onCleanup) => {
const dom = newVal ? app : div
function handler () {
console.log(newVal ? '點擊app' : '點擊div')
}
dom.addEventListener('click', handler)
onCleanup(() => {
dom.removeEventListener('click', handler)
})
},
{ immediate: true }
)
button.onclick = () => {
flag.value = !flag.value
}
</script>
</body>
我們現在在監聽的時候綁一個監聽事件,onCleanup
接受一個 callback 函式,我們在函式中寫移除事件。
onCleanup
註冊的 callback 函式會在下一次 watch
callback 即將執行**之前被呼叫,**這個時機確保了我們在新的副作用出現前,清理掉上一個過期的 Side Effect。
你會發現回去點 app
,它不會再被觸發了。
export function watch(source, cb, options) {
...
...
let cleanup = null
function onCleanup(cb) {
cleanup = cb
}
function job() {
if(cleanup) {
// 確認是不是要清理之前的 sideEffect 函式
cleanup()
cleanup = null
}
// 執行 effect 的函式,得到新的數值,不能直接執行 getter,因為要收集依賴
const newValue = effect.run()
cb(newValue, oldValue, onCleanup)
oldValue = newValue
}
...
...
}
我們儲存外部傳入 onCleanup
的 callback 函式,把它儲存到變數之中,接著判斷,如果 onCleanup
傳入函式存在,在每次執行 job
函式之前,先執行一次清理函式。
onCleanup
是 watch
API 中一個很重要但容易被忽略的特性。它為開發者提供了一個標準化的機制,來應對 Side Effect 帶來的挑戰:資源洩漏(如未移除的事件監聽器)與非同步競爭條件(如過期的網路請求)。
同步更新《嘿,日安!》技術部落格