前言
在欧阳的上一篇 这应该是全网最详细的Vue3.5版本解读文章中有不少同学对Vue3.5新增的onWatcherCleanup
有点疑惑,这个新增的API好像和watch API
回调的第三个参数onCleanup
功能好像重复了。今天这篇文章来讲讲新增的onWatcherCleanup
函数的使用场景:封装一个自动cancel的fetch函数
。
加入欧阳的高质量vue源码交流群、欧阳平时写文章参考的多本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函数。
最后推荐一下欧阳自己写的开源电子书vue3编译原理揭秘,看完这本书可以让你对vue编译的认知有质的提升,并且这本书初、中级前端能看懂。完全免费,只求一个star。