消除异步传染性
刀刀
4/7/2025
0 字
0 分钟
题目
下面来看一段代码:
js
async function getUser() {
return await fetch('http://xxx/demo/profile')
.then(res => res.json());
}
async function m1() {
return await getUser();
}
async function m2() {
return await m1();
}
async function m3() {
return await m2();
}
async function main() {
const user = await m3();
console.log(user)
}
有一个函数 getUser
,调用接口获取数据,通过 async
和 await
获取异步数据,此时该函数添加了 async
后会变为 Promise
异步函数。后续函数想要使用该函数也需要再一次 async
和 await
,如上方代码所示。
这样虽说能够解决,但是代码看起来冗杂,且让函数从纯函数变成了异步函数,带来了副作用。如何把那些函数都变成同步函数,且最终能够打印出结果?
首先需要定位最主要的问题在哪里。毋庸置疑,在最开始的 getUser
函数,因为需要获取到数据,因此才开始一系列的 async
和 await
。因此需要在这里做文章。
在取消掉 async
和 await
后函数如下:
js
async function getUser() {
return await fetch('http://xxx/demo/profile')
.then(res => res.json());
}
此时需要它立即返回结果,但是由于接口请求还没回来,它无法返回响应的数据,但是又必须返回一个结果,那么怎么解决呢?
思路
报错,对的没错,这里既然无法返回接口响应的数据,那么就返回一个报错信息过去。
这个思路可以用一个图来解释,如下图所示:
首先函数开始,执行 fetch
函数,由于它是异步操作,因此无法等待到它数据返回,直接返回错误信息,运行结束。
此时网络请求在底部默默地请求着数据,拿到数据后缓存起来,然后重新执行函数,也就是说这个函数会执行两次,这要求这个函数没有副作用。
改造
上方代码不能做侵入性修改,那么只能从终点做文章。重新声明一个 run
函数,定义一个全局的 fetch
函数,该函数做两件事:
- 判断是否有缓存,有缓存交付缓存结果;没有缓存则缓存一个包含请求状态、请求结果、请求信息的对象,发送请求,在回调保存对应的结果和修改请求状态,返回
Promise
报错 - 通过
try...catch
捕获错误,在错误类型等于抛出的Promise
才重新执行
代码
js
function run(func) {
let catch = [] // 保存缓存的数组
let i = 0
const _originalFetch = window.fetch
window.fetch = (...args) => {
if(catch[i]) {
// 有缓存,判断状态,调用成功返回结果,失败返回报错信息,后续不再调用
if(catch[i].status === 'fulfilled') {
return catch[i].data
} else if(catch[i].status === 'rejected') {
throw catch[i].err
}
}
// 没有缓存,先保存一个请求状态、最终结果、错误信息的缓存数据对象
let result = {
status: 'pending',
data: null,
err: null
}
catch[i++] = result // 下一次调用存储下一个结果
// 发请求,用旧的fetch
const prom = _originalFetch(...args)
.then(res => res.json())
.then(res => {
// 成功
result.status = 'fulfilled'
res.data = res
}, err => {
// 失败
result.status = 'rejected'
res.err = err
})
// 接口还没数据,直接返回报错,报错要是promise
throw prom
}
// 执行函数,捕获错误
try {
func()
} catch(err) {
// 只有是promise错误才重新执行
if(err instanceof Promise) {
const reRun = () => {
func()
}
err.then(reRun, reRun)
}
}
}
run(main)
拓展
在 react
中,Suspense
组件的原理其实是一样的。
jsx
function ProfilePage() {
return (
<Suspense callback={<h1>Loading...</h1>}>
<ProfileDetails />
</Suspense>
)
}
function ProfileDetails() {
const user = yserResource.read()
return <h1>{user.name}</h1>
}
组件 ProfileDetails
本来是要同步的,但它这里是异步的,原理和前面的思路一样,等待 Promise
执行完毕后再调用一次。