欢迎大家关注我的微信公众号:编程进阶录
,与作者一起学习进步。
在使用 vueuse
开发项目的过程当中,有会用到 useFetch
这个 composition api
,发现它能够同时支持开发者同步调用和异步调用,比较好奇怎么实现的,这篇文章带你一探究竟。
普通调用的写法如下面所示,常用在页面加载的时候:
ts
import { useFetch } from '@vueuse/core'
const { isFetching, error, data } = useFetch(url)
异步调用的写法,常用在用户交互的事件处理函数当中,如下所示:
ts
import { useFetch } from '@vueuse/core'
const { isFetching, error, data } = await useFetch(url)
看到这里,你会不会也和我一样好奇为啥它一个 API 能够同时支持同步和异步调用呢?
那么这就是这篇文章的意义,对 useFetch 这个 API 进行源码分析,找出其实现该功能的关键点,然后我们也自己封装一个同时支持普通调用和异步调用的 composition api
。
源码分析
获得源码
阅读源码之前,我们需要先获得 vueuse 的源码,这里我直接克隆仓库的命令。
bash
git clone https://github.com/vueuse/vueuse.git
为了方便寻找,我这里直接贴出 useFetch 的源码链接: github.com/vueuse/vueu...
我们从 useFetch 的函数重载签名上,可以看到返回值是一个 PromiseLike 的对象,如下图所示:
PromiseLike 这个 TypeScript 接口,我们可以点进去看一下,发现就是一个 then 方法的接口,让我们再回忆一下 Promise 的相关知识,在 Promise A+ 规范当中,只要一个对象是 thenable 的,那么就可以认定这个对象是个 Promise 对象。

而 Promise 对象就可以使用 async / await
来获得 Promise 的结果,所以说只需要 useFetch 是一个返回带 then 函数的对象,那么就可以实现 await 进行调用来获得结果,如下所示,最后控制台打印的结果就是 resolve 出来的 1。
js
const p = {
then(res, rej){
res(1)
}
}
;(async () => {
const result = await p
console.log(result)
})()
我们也可以看到 useFetch 返回出来的对象也是一个带有 then 函数的对象,其中的 shell 函数就是我们使用普通函数调用时候是能够获得的 data、isFetching 等等响应式数据。
我们先不考虑返回对象当中的 then 函数是怎么写的,先来实现一个简单的同步调用返回响应式数据的 composition api。
这里我贴出最简单的写法,假设请求都由调用方传给我们封装的函数,那么写法大概如下所示:
ts
import { readonly, ref } from 'vue'
function useFetch(api: () => Promise<any>) {
const data = ref()
const isFinished = ref(false)
const shell = {
data,
isFinished: readonly(isFinished),
}
function execute() {
isFinished.value = false
api()
.then((res) => {
data.value = res
})
.finally(() => {
isFinished.value = true
})
}
execute()
return {
...shell,
execute
}
}
;(async () => {
const { data, isFinished } = useFetch(() => fetch('https://jsonplaceholder.typicode.com/todos/1').then((res) => res.json()))
console.log(data.value)
console.log(isFinished.value)
setTimeout(() => {
console.log(data.value)
console.log(isFinished.value)
}, 2000);
})()
可以测试一下我们写的正确性,
可以看到,和我们预期的一样,在发起请求的时候,data 没有数据,isFinished 也为 false,拿到结果以后,都相应的发生了改变。
很好,到这里为止,我们已经成功的一半了,使用普通函数调用的方式来使用我们自己封装的 useFetch,接下来我们继续阅读一下 vueuse 源码里面返回的带有 then 函数的对象,看看它是怎么样做到能够直接 await 来调用 useFetch 来获得结果的。

再回顾一个 Promise 的知识,then 函数是一个使用两个回调函数作为参数的函数,可以看到 useFetch 使用了一个 waitUntilFinished()
的函数获得一个 Promise,将我们调用时候的成功和失败回调注册给这个 Promise,这里可能会有点绕,我们先直接看 waitUntilFinished
函数的源码。

可以看到它确实是返回了一个 Promise 对象,但是在 Promise 的执行其当中又调用了一个 until 方法,将当前 useFetch 的状态传了进去,然后调用 toBe(true) 获得一个 promise 并且等该 promise 完成时候调用了 resolve(shell)。
这里我大概解释一下 until(isFinished).toBe(true)
这个的含义,就是等到 isFinished 这个响应式变量的值变成 true(即 isFinished.value = true
) 这行代码执行的时候,就会把结果 resolve 出来,也就是将请求到的结果传给 useFetch 返回的对象里的 then 方法的 onFulfilled 回调函数去,即我们成功的使用 await 调用 useFetch 并且获得了结果。
我们优化一下我们自己写 useFetch,这里先借用一下 vueuse 的 until 工具,并且添加一些简单的 ts 类型(直接用 any 了,项目当中实际使用可以结合泛型)。
ts
import { readonly, ref } from 'vue'
import { until } from '@vueuse/core'
function useFetch(api: () => Promise<any>): PromiseLike<any> & {
data: any
isFinished: any
execute: () => void
} {
const data = ref()
const isFinished = ref(false)
const shell = {
data,
isFinished: readonly(isFinished),
execute,
}
function waitUntilFinished() {
return new Promise((rs, rj) => {
until(isFinished)
.toBe(true)
.then(() => rs(shell))
.catch((err) => rj(err))
})
}
function execute() {
isFinished.value = false
api()
.then((res) => {
data.value = res
})
.finally(() => {
isFinished.value = true
})
}
execute()
return {
...shell,
then(onFulfilled: any, onRejected: any) {
return waitUntilFinished().then(onFulfilled, onRejected)
},
}
}
;(async () => {
const { data, isFinished } = await useFetch(() =>
fetch('https://jsonplaceholder.typicode.com/todos/1').then((res) => res.json())
)
console.log(data.value)
console.log(isFinished.value)
})()
测试一下效果,ok,没问题,我们已经成功的封装一个支持普通函数调用和异步调用的 composition api
。

until - 响应式 Promise 工具函数
通过上面的源码分析,我们已经成功的借助 vueuse 的 until 工具函数,成功的实现一个同时支持普通调用和异步调用的 composition api。
接下来,我们需要通过阅读学习一个 until 工具的源码,让我们自己也可以实现一个 until 函数。
源码地址: github.com/vueuse/vueu...

可以看到 until 调用了一个 createUntil 的函数,并且把我们传入的响应式数据传给了它,查看 createUntil 函数的源码可以发现它返回的一个带有 toBe 方法的对象。

那么我们就可以直接查看 toBe 方法的源码了,源码截图如下:
可以看到,toBe 函数当中有使用 Promise.race 对一个带有监听的的 Promise 和一个超时处理的 Promise 进行了处理,这里我们只需要看 watcher 这个 Promise 即可,其他的可以自行了解,这里不多做介绍。
监听的数据为我们传进来的 r (也就是 useFetch 当中的 isFinished)以及调用 toBe 时候传进来的值,如果 r 和 value 的想等,isNot 默认值为 false,即 isNot !== (v1 === 2)
这个条件成立,直接调用 Promise 的 resolve 函数表示请求已经完成了,同时停止监听两个值的变化。
这里用 watch 用的非常的巧妙,也体现了开源库作者的智慧,非常厉害,当时看到这个地方的时候感觉这个设计简直太妙了。
那接下来我们就动手实现一个自己的 until 函数,注意 value 可能是普通值,需要监听它的变化的话需要使用 () => value
。
ts
import { MaybeRefOrGetter, readonly, ref, watch } from 'vue'
function until(r: any) {
function toBe(value: MaybeRefOrGetter<any>) {
return new Promise((resolve, _) => {
const stop = watch(
[r, () => value],
([v1, v2]) => {
if (v1 === v2) {
stop()
resolve(r)
}
}
)
})
}
return {
toBe,
}
}
到此为止,我们已经成功自己实现了一个同时支持普通调用和异步调用的 composition api ,整体源码如下所示:
ts
import { MaybeRefOrGetter, readonly, ref, watch } from 'vue'
function until(r: any) {
function toBe(value: MaybeRefOrGetter<any>) {
return new Promise((resolve, _) => {
const stop = watch([r, () => value], ([v1, v2]) => {
if (v1 === v2) {
stop()
resolve(r)
}
})
})
}
return {
toBe,
}
}
function useFetch(api: () => Promise<any>): PromiseLike<any> & {
data: any
isFinished: any
execute: () => void
} {
const data = ref()
const isFinished = ref(false)
const shell = {
data,
isFinished: readonly(isFinished),
execute,
}
function waitUntilFinished() {
return new Promise((rs, rj) => {
until(isFinished)
.toBe(true)
.then(() => rs(shell))
.catch((err) => rj(err))
})
}
function execute() {
isFinished.value = false
api()
.then((res) => {
data.value = res
})
.finally(() => {
isFinished.value = true
})
}
execute()
return {
...shell,
then(onFulfilled: any, onRejected: any) {
return waitUntilFinished().then(onFulfilled, onRejected)
},
}
}
;(async () => {
const { data: result1, isFinished: isFinished1 } = await useFetch(() =>
fetch('https://jsonplaceholder.typicode.com/todos/1').then((res) => res.json())
)
console.log(result1.value)
console.log(isFinished1.value)
const { data, isFinished } = useFetch(() =>
fetch('https://jsonplaceholder.typicode.com/todos/1').then((res) => res.json())
)
console.log(data.value)
console.log(isFinished.value)
setTimeout(() => {
console.log(data.value)
console.log(isFinished.value)
}, 2000)
})()
测试效果如下,非常完美:
通过这次源码的学习,我们成功的掌握了怎么写出一个同时支持同步和异步调用的 composition api,如果你觉得有非常大的收获的话,欢迎关注我的公众号:编程进阶录
,与我 共同进步。