跳转到内容

消除异步传染性

刀刀

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 ,调用接口获取数据,通过 asyncawait 获取异步数据,此时该函数添加了 async 后会变为 Promise 异步函数。后续函数想要使用该函数也需要再一次 asyncawait ,如上方代码所示。

这样虽说能够解决,但是代码看起来冗杂,且让函数从纯函数变成了异步函数,带来了副作用。如何把那些函数都变成同步函数,且最终能够打印出结果?

首先需要定位最主要的问题在哪里。毋庸置疑,在最开始的 getUser 函数,因为需要获取到数据,因此才开始一系列的 asyncawait 。因此需要在这里做文章。

在取消掉 asyncawait 后函数如下:

js
async function getUser() {
  return await fetch('http://xxx/demo/profile')
  	.then(res => res.json());
}

此时需要它立即返回结果,但是由于接口请求还没回来,它无法返回响应的数据,但是又必须返回一个结果,那么怎么解决呢?

思路

报错,对的没错,这里既然无法返回接口响应的数据,那么就返回一个报错信息过去。

这个思路可以用一个图来解释,如下图所示:

思路

首先函数开始,执行 fetch 函数,由于它是异步操作,因此无法等待到它数据返回,直接返回错误信息,运行结束。

此时网络请求在底部默默地请求着数据,拿到数据后缓存起来,然后重新执行函数,也就是说这个函数会执行两次,这要求这个函数没有副作用。

改造

上方代码不能做侵入性修改,那么只能从终点做文章。重新声明一个 run 函数,定义一个全局的 fetch 函数,该函数做两件事:

  1. 判断是否有缓存,有缓存交付缓存结果;没有缓存则缓存一个包含请求状态、请求结果、请求信息的对象,发送请求,在回调保存对应的结果和修改请求状态,返回 Promise 报错
  2. 通过 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 执行完毕后再调用一次。