Vue3.5 封装一个自动cancel的fetch函数
有不少同学对 Vue3.5 新增的onWatcherCleanup
有点疑惑,这个新增的 API 好像和watch API
回调的第三个参数onCleanup
功能好像重复了。今天这篇文章来讲讲新增的onWatcherCleanup
函数的使用场景:封装一个自动 cancel 的 fetch 函数
。
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.5 封装一个自动cancel的fetch函数