跳转到内容

消除异步传染性

刀刀
0字
0分钟
2026/8/16

题目

下面来看一段代码:

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 执行完毕后再调用一次。

贡献者

The avatar of contributor named as v_duyilin v_duyilin
The avatar of contributor named as duyidao duyidao
The avatar of contributor named as 刀刀 刀刀
The avatar of contributor named as 平安喜乐 平安喜乐

页面历史

刀刀博客累计访客 人;文档累计访问量共