Vue 3.5 更新,onWatcherCleanup 與 onCleanup
Vue 3.5 更新了!除了其他改進與實用功能外,有一個很有趣的新 API,叫做 onWatcherCleanup
。
不過原本的 watch 已經有 onCleanup
了,所以到底差在哪呢?
文件 Side Effect Cleanup 一節中提到的用法為:
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// callback logic
})
onWatcherCleanup(() => {
// abort stale request
controller.abort()
})
})
不過 watch 本身就有 onCleanup,所以應該可以這樣寫。
watch(id, (newId, oldId, onCleanup) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// callback logic
})
onCleanup(() => {
// abort stale request
controller.abort()
})
})
文件 onWatcherCleanup 章節也沒有其他說明。
說不定實際效果不一樣?讓我們來實測看看吧。(๑•̀ㅂ•́)و✧
這裡我們使用一個免費的 API 來測試,順便取得貓貓冷知識 XD。
實際測試
<script setup>
import { ref, watch, onWatcherCleanup } from 'vue'
const id = ref('cod')
const data = ref([])
watch(id, (newId, oldId, onCleanup) => {
const controller = new AbortController()
fetch(
`https://cat-fact.herokuapp.com/facts?id=${newId}`,
{ signal: controller.signal }
)
.then((res) => res.json())
.then((value) => {
data.value = value;
})
.catch((error) => {
console.error(error);
})
onWatcherCleanup(() => {
console.log('[ onWatcherCleanup ] : ', controller);
controller.abort()
}) // */
/* onCleanup(() => {
console.log('[ onCleanup ] : ', controller);
controller.abort()
}) // */
})
function getData() {
id.value = crypto.randomUUID();
}
</script>
<template>
<button @click="getData()">取得貓貓小知識</button>
<p v-for="datum, i in data">
{{ i + 1 }}. {{ datum.text }}
</p>
</template>
就一個簡單的按鈕,點擊後會取得貓貓小知識,並顯示在畫面上。
每次 watch 觸發時,會呼叫上一次的 onCleanup 或 onWatcherCleanup,這樣就可以清理上一次的副作用。
所以讓我們快速點擊按鈕兩下,觀察一下 DevTools 之 Network 的結果。
可以注意到上次的 fetch 順利取消了!
現在讓我們把 onWatcherCleanup 註解掉,改用 onCleanup,再來觀察一下。
見證奇蹟的時刻到了!
結果一模一樣!╮(╯▽╰)╭
...嗯,以上例子的確看不出差別,但是不代表 onWatcherCleanup 沒有用途,讓我們再來看看其他情境吧。
onWatcherCleanup 使用情境
目前來看 onWatcherCleanup 比較類似 getCurrentScope、onScopeDispose 這類底層 API,平常不會特別使用,一般業務邏輯用 onCleanup 就好。(想統一用 onWatcherCleanup 也是可以 XD)
不過如果你希望「在 watch 中重新執行時,會自動清理上一次動作」的實用 API,這時候 onWatcherCleanup 就派上用場了。
概念上有點類似 VueUse 的 useEventListener,此 API 可以在元件中註冊事件,並在元件被銷毀時自動移除。
import { useEventListener } from '@vueuse/core'
// 像這樣註冊事件,不用手動 removeEventListener
useEventListener(document, 'visibilitychange', (evt) => {
console.log(evt)
})
只是 useEventListener 的目標是「整個元件的 effect scope」,而 onWatcherCleanup 的目標是「單一 watcher scope」。
讓我們延伸剛剛的例子,設計一個在 watch 使用時會自動取消前一次請求,並在元件解除後也會自動取消請求的 API。
新增一個 useCatFact。
import { onWatcherCleanup, shallowRef, onBeforeUnmount } from 'vue'
export function useCatFact() {
const abortController = shallowRef();
/** 元件解除前,自動取消請求 */
onBeforeUnmount(() => {
abortController.value?.abort()
});
/** 取得貓貓小知識 */
function getFact(id) {
return new Promise((resolve, reject) => {
const controller = new AbortController()
fetch(
`https://cat-fact.herokuapp.com/facts?id=${id}`,
{ signal: controller.signal }
)
.then((res) => res.json())
.then((value) => resolve(value))
.catch((error) => {
// 忽略終止錯誤
if (error.name === 'AbortError') return;
reject(error);
})
/**
* 在 watcher 範圍內會自動取消前一次請求
*
* 第二個參數 true 表示忽略不在 watcher 範圍內警告
*/
onWatcherCleanup(() => {
controller.abort()
}, true)
abortController.value = controller;
})
}
return {
getFact,
}
}
接著在元件中使用。
<script setup>
import { ref, watch } from 'vue'
import { useCatFact } from './useCatFact.js';
const id = ref('cod')
const data = ref([])
const { getFact } = useCatFact();
watch(id, async (newId) => {
data.value = await getFact(newId);
})
async function getData() {
id.value = crypto.randomUUID();
}
</script>
<template>
<button @click="getData()">取得貓貓小知識</button>
<p class="tip" v-for="datum, i in data">
{{ i + 1 }}. {{ datum.text }}
</p>
</template>
現在連續點擊按鈕,可以看到前次請求都會取消,而且元件解除時也會自動取消請求。
onCleanup
傳到 useCatFact
裡,不也可以嗎?」的確可以,就看設計需求與風格了,像是剛剛提到的
useEventListener
就是自動處理的方式。( •̀ ω •́ )✧可以看到程式碼變得相當乾淨,不需要手動 onCleanup,也不用擔心忘記取消請求,非常方便。∠( ᐛ 」∠)_
除了發送 HTTP 請求,這在設計動畫也十分實用,可以有效防止同一物件上的動畫重複執行。
舉一個動畫的例子,這裡使用 anime.js。
設計一個名為 useBoxAnimation 的動畫工具,可以對指定元素執行各類預先定義好的動畫。
import anime from 'animejs'
export function useBoxAnimation() {
/** 動畫目標元素 */
const boxRef = ref<HTMLElement>();
/** 元件解除前,刪除動畫 */
onBeforeUnmount(() => {
anime.remove(boxRef.value)
});
function translate(x, y) {
anime({
targets: boxRef.value,
translateX: x,
translateY: y,
easing: 'spring(1, 80, 10, 0)'
});
onWatcherCleanup(() => {
anime.remove(boxRef.value)
}, true)
}
function rotate(deg) {
anime({
targets: boxRef.value,
rotate: deg
});
onWatcherCleanup(() => {
anime.remove(boxRef.value)
}, true)
}
return {
boxRef,
translate,
rotate,
}
}
以上只是我目前的心得與理解,如果有更多的使用情境或是更深入的解釋,歡迎大家一起討論!(´▽`ʃ♡ƪ)
總結 🐟
- 認識 onCleanup 與 onWatcherCleanup
- onWatcherCleanup 適合用在封裝後的功能,一般業務邏輯用 onCleanup 就好
- 官方文件的 onWatcherCleanup 例子舉的不太好,看不出與 onCleanup 的應用差異,害我一開始看得一頭霧水 XD
最後再次感謝 dog 大大提供的資訊,也歡迎大家加入 Vue.js 社群
一起討論 Vue 相關議題!(/≧▽≦)/