前言
在歐陽的上一篇 這應該是全網最詳細的Vue3.5版本解讀文章中有不少同學對Vue3.5新增的onWatcherCleanup
有點疑惑,這個新增的API好像和watch API
回撥的第三個引數onCleanup
功能好像重複了。今天這篇文章來講講新增的onWatcherCleanup
函式的使用場景:封裝一個自動cancel的fetch函式
。
關注公眾號:【前端歐陽】,給自己一個進階vue的機會
watch回撥的第三個引數onCleanup
有些同學可能還不清楚watch
回撥的第三個引數onCleanup
,我們先來看個demo,程式碼如下:
watch(id, (value, oldValue, onCleanup) => {
console.log("do something");
onCleanup(() => {
console.log("cleanup");
});
});
watch
回撥的前兩個引數大家應該很熟悉,分別是value
新的值,oldValue
舊的值。
第三個引數onCleanup
大家平時可能用的不多,這是一個回撥函式,當watch
的值改變後或者元件銷燬前就會執行onCleanup
傳入的回撥。
在上面的demo中就是變數id
改變時會觸發onCleanup
中的回撥,進而console
列印"cleanup"
字串。又或者所在的元件銷燬前也會觸發onCleanup
中的回撥,進而console
列印"cleanup"
字串。
那我們在onCleanup
中可以幹嘛呢?
答案是可以清理副作用,比如在watch中使用setInterval
初始化一個定時器。那麼我們就可以在onCleanup
的回撥中清理掉定時器,無需去元件的beforeUnmount
鉤子函式去統一清理。
onWatcherCleanup
函式
onWatcherCleanup
函式的作用和watch
回撥的第三個引數onCleanup
差不多,也是當watch
的值改變後或者元件銷燬前就會執行onWatcherCleanup
傳入的回撥。
使用方法也很簡單,程式碼如下:
import { watch, onWatcherCleanup } from "vue";
watch(id, () => {
console.log("do something");
onWatcherCleanup(() => {
console.log("cleanup");
});
});
從上面的程式碼可以看到onWatcherCleanup
的用法其實和watch
回撥的第三個引數onCleanup
差不多,區別在於這裡的onWatcherCleanup
是從vue中import匯入的。
除了從vue中import匯入的區別以外,還有一個區別是onWatcherCleanup
不光在watch
中可以使用,在watchEffect
中同樣也可以使用。比如下面這樣的:
watchEffect(() => {
console.log("do something in watchEffect", id.value);
onWatcherCleanup(() => {
console.log("cleanup watchEffect");
});
});
和前面的例子一樣,上面的程式碼中id
的值改變後或者元件銷燬時也會執行onWatcherCleanup
函式中的console.log
列印。
onWatcherCleanup
函式是從vue中import匯入的,那麼這意味著onWatcherCleanup
函式的呼叫可以寫在任意地方,只要最終經過函式的層層呼叫後還是在watch
或者watchEffect
的回撥中就可以。
利用上面的這一特點我們可以使用onWatcherCleanup
做到一些onCleanup
做不到的事情,比如:封裝一個自動cancel
的fetch
函式。
封裝自動cancel的fetch函式
在講這個之前我們先來了解一下如何cancel
一個fetch
函式。
這裡涉及到AbortController
介面,AbortController
介面表示一個控制器物件,允許你根據需要中止一個或多個 Web 請求。
下面這個是cancel
取消一個請求的demo,程式碼如下:
const controller = new AbortController();
const res = await fetch(url, {
...options,
signal: controller.signal,
});
setTimeout(() => {
controller.abort();
}, 500);
首先使用new AbortController()
建立一個控制器物件controller
。
其中的controller.signal
返回一個 AbortSignal
物件例項,可以用它來和非同步操作進行通訊或者中止這個操作。
在我們這裡把controller.signal
作為signal
選項直接傳給fetch函式就可以了。
最後就是可以使用controller.abort()
將fetch請求取消掉,在上面的demo中是如果超過500ms請求還沒完成,那麼就執行controller.abort()
將fetch請求取消掉。
有了前面的知識鋪墊,我們先來看看使用“自動cancel
的fetch
函式”的地方,程式碼如下:
<script setup lang="ts">
import { watch, ref, watchEffect, onWatcherCleanup } from "vue";
import myFetch from "./myFetch";
const id = ref(1);
const data = ref(null);
watch(id, async () => {
const res = await myFetch(`http://localhost:3000/api/${id.value}`, {
method: "GET",
});
console.log(res);
data.value = res;
});
</script>
<template>
<p>data is: {{ data }}</p>
<button @click="id++">id++</button>
</template>
在上面的例子中使用watch
監聽了變數id
,在監聽的回撥中會使用封裝的myFetch
函式請求介面。
上面的例子大家平時應該經常遇到,如果id
的值變化很快,但是服務端介面請求需要2秒才能完成,這時我們期望只有最後一次id
的值改變觸發的請求才需要完成,其他請求都cancel取消掉。
如果在myFetch
請求的過程中元件被銷燬了,此時我們也期望能夠將請求cancel取消掉。
在Vue3.5之前想要去實現上面的這兩個需求很麻煩,但是有了Vue3.5的onWatcherCleanup
函式後就非常容易了。
這個是封裝的自動cancel
的fetch
函式,myFetch.ts
檔案程式碼如下:
import { getCurrentWatcher, onWatcherCleanup } from "vue";
export default async function myFetch(url: string, options: RequestInit) {
const controller = new AbortController();
if (getCurrentWatcher()) {
onWatcherCleanup(() => {
controller.abort();
});
}
const res = await fetch(url, {
...options,
signal: controller.signal,
});
let json;
try {
json = await res.json();
} catch (error) {
json = {
code: 500,
message: "JSON format error",
};
}
return json;
}
由於onWatcherCleanup
函式是從vue中import匯入,那麼我們就可以在自己封裝的myFetch
函式中匯入和使用他。
在onWatcherCleanup
函式的回撥中我們執行了controller.abort()
,前面已經講過了當watch
或者watchEffect
的回撥執行前或者元件解除安裝前就會執行裡面的onWatcherCleanup
註冊的回撥。我們這裡的myFetch
是在watch
中呼叫的,當然也會觸發裡面的onWatcherCleanup
註冊的回撥。
在onWatcherCleanup
的回撥中執行了controller.abort()
,前面我們講過了執行controller.abort()
就會將正在請求的fetch函式給cancel取消掉。
就這麼簡單的就實現了前面的兩個需求:
需求一:如果id
的值變化很快,但是服務端介面請求需要2秒才能完成,這時我們期望只有最後一次id
的值改變觸發的請求才需要完成,其他請求都cancel取消掉。下面這個是變數id在短時間內多次修改的gif效果圖:
從上面的gif圖可以看到只有最後一個請求是完成了的,其他請求全部被cancel掉。
需求二:如果在myFetch
請求的過程中元件被銷燬了,此時我們也期望能夠將請求cancel取消掉。下面這個是元件解除安裝時gif效果圖:
從上圖中可以看到在解除安裝元件時元件正在從服務端請求資料,此時請求會自動cancel掉。
細心的小夥伴發現了在myFetch
函式中,onWatcherCleanup
函式外面套了一個getCurrentWatcher
的判斷,程式碼如下:
import { getCurrentWatcher, onWatcherCleanup } from "vue";
export default async function myFetch(url: string, options: RequestInit) {
// ...省略
if (getCurrentWatcher()) {
onWatcherCleanup(() => {
controller.abort();
});
}
// ...省略
}
當watch或者watchEffect監聽的值改變後onWatcherCleanup
的回撥就會觸發,所以onWatcherCleanup
的執行是由其所在的watch或者watchEffect觸發的。
如果onWatcherCleanup
不在watch或者watchEffect的回撥中執行,那麼當然onWatcherCleanup
中的回撥也永遠不會執行。
可能有的小夥伴有疑問,你這裡的onWatcherCleanup
是在myFetch
中執行的,也沒在watch或者watchEffect的回撥中執行吖?
答案是myFetch
函式的執行是在watch中執行的,myFetch
然後再去執行onWatcherCleanup
。
而getCurrentWatcher()
函式就會返回當前正在執行回撥的watch或者watchEffect,如果當前myFetch
不是在watch或者watchEffect的回撥中執行的,那麼getCurrentWatcher()
函式的返回值就是空,所以這種情況就不需要去執行onWatcherCleanup
函式了。
最後值得一提的是onWatcherCleanup
不能在await後面執行,比如下面這樣的程式碼:
import { getCurrentWatcher, onWatcherCleanup } from "vue";
export default async function myFetch(url: string, options: RequestInit) {
const controller = new AbortController();
const res = await fetch(url, {
...options,
signal: controller.signal,
});
let json;
try {
json = await res.json();
} catch (error) {
json = {
code: 500,
message: "JSON format error",
};
}
// ❌ 錯誤的寫法
if (getCurrentWatcher()) {
onWatcherCleanup(() => {
controller.abort();
});
}
return json;
}
在上面的程式碼中我們將onWatcherCleanup
呼叫放在了await fetch()
的後面,這種寫法onWatcherCleanup
註冊的回撥是不會執行的。
為什麼在await
後面的onWatcherCleanup
註冊的回撥永遠不會執行呢?
答案是js的await相當於註冊了一個回撥函式去執行await後的程式碼,當await等待結束後再去執行這個回撥函式,從而執行await後的程式碼。
await以及之前的程式碼確實是在watch回撥中執行的,我們這裡的onWatcherCleanup
就是await後面的程式碼,await後面的程式碼是在一個新的回撥中執行的,也就是watch“回撥中”的“回撥中”執行的。
當onWatcherCleanup
執行時已經不知道當前正在執行的watch回撥是誰了,所以onWatcherCleanup
的回撥也沒註冊上。當watch的變數修改時或者元件解除安裝時onWatcherCleanup
註冊的回撥永遠也不會執行。
總結
當watch
或者watchEffect
監聽的變數修改時,以及元件解除安裝時,會去執行他們回撥中使用onWatcherCleanup
註冊的回撥函式。並且onWatcherCleanup
是從vue中import匯入的,使得我的可以在任意地方執行onWatcherCleanup
函式。利用這兩個特性我們就可以封裝一個自動cancel的fetch函式。
關注公眾號:【前端歐陽】,給自己一個進階vue的機會
另外歐陽寫了一本開源電子書vue3編譯原理揭秘,看完這本書可以讓你對vue編譯的認知有質的提升。這本書初、中級前端能看懂,完全免費,只求一個star。