likes
comments
collection
share

Promise.all和promise.allSettled的用法,promise.all的错误捕获方法

作者站长头像
站长
· 阅读数 21

Promise.all

用于优化多个同时处理的异步请求,降低时间

例如

async function getPageData() {
  const user = await fetchUser()
  const product = await fetchProduct()
}

在此函数中,我们依次等待获取用户数据和产品数据。

但是这两个相互之间不存在依赖依赖,所以我们不必等待一个完成再发出对下一个的请求。

相反,我们可以同时触发两个请求,并同时等待。这样优化此功能以在短短一半的时间。

async function getPageData() {
  const [user, product] = await Promise.all([
    fetchUser(), fetchProduct()
  ])
}

如果我们想象每个请求都需要 1 秒来响应,而在我们的原始函数中,我们会连续等待两个请求,总共需要 2 秒才能完成,在这个新函数中,我们同时等待两个请求,所以我们函数在 1 秒内完成——时间减半!

缺点

首先,我们在这里根本不处理错误。

所以你可以说“当然,我会把它放在一个大的 try-catch 块中”。

async function getPageData() {
  try {
    const [user, product] = await Promise.all([
      fetchUser(), 
      fetchProduct()
    ])
  } catch (err) {
    // 🚩 这会有一个大问题
  }
}

但这其实有一个大问题。

假设fetchUser首先完成并出现错误。这将触发我们的 catch 块,然后继续执行该功能。

如果fetchProduct之后出错,这将不会触发 catch 块。那是因为我们的功能已经继续了。catch 代码已经运行,函数已经完成。

因此,这将导致未处理fetchProductpromise.reject

解决方法1

解决上述问题的一种方法是将函数传递给.catch(),例如:

// 用户捕获错误自定义去处理
function handle(err) {
  alertToUser(err) 
  saveToLoggingService(err)
}

function onReject(err) {
  handle(err)
  return err
}

async function getPageData() {
  const [user, product] = await Promise.all([
    fetchUser().catch(onReject), // ⬅️
    fetchProduct().catch(onReject) // ⬅️
  ])

  if (user instanceof Error) {
    handle(user) // ✅
  }
  if (product instanceof Error) {
    handle(product) // ✅
  }
}

在这种情况下,如果我们得到一个错误,我们返回处理错误和它本身。所以现在我们的结果userproduct对象要么是一个Error,我们可以检查它instanceof,要么是我们没有报错,正确的结果。

这还不错,解决了我们之前的问题。

但是,这里的主要缺点是我们需要确保我们始终在异步请求后跟着.catch(onReject)。遗憾的是,这很容易被遗漏,而且也不是最容易为其编写防弹 eslint 规则的方法。

解决方法2

我们并不总是需要在创建Promise后立即await。另一种几乎相同的技术是:

**同步请求、异步等待 **

async function getPageData() {
  // 同时触发两个请求
  const userPromise = fetchUser().catch(onReject)
  const productPromise = fetchProduct().catch(onReject)

  
  const user = await userPromise
  const product = await productPromise

  // 处理错误
  if (user instanceof Error) {
    handle(user)
  }
  if (product instanceof Error) {
    handle(product)
  }
}

因为我们在等待任何一个之前触发了每个请求,所以这个版本与我们上面使用Promise.all.

此外,在这种格式中,try/catch如果我们愿意,我们可以安全地使用而不会出现我们之前遇到的问题:

async function getPageData() {
  const userPromise = fetchUser().catch(onReject)
  const productPromise = fetchProduct().catch(onReject)

  // Try/catch each
  try {
    const user = await userPromise
  } catch (err) {
    handle(err)
  }
  try {
    const product = await productPromise
  } catch (err) {
    handle(err)
  }
}

在这三者之间,我个人比较喜欢这个Promise.all版本,因为“这两个东西一起等”感觉更地道。但话虽如此,我认为这只是归结为个人喜好

解决方案 3

Promise.allSettled

Promise.allSettled

JavaScript 中内置的另一种解决方案是使用Promise.allSettled.

Promise.allSettled,我们得到一个包含每个承诺结果的值或错误的结果对象。

async function getPageData() {
  const [userResult, productResult] = await Promise.allSettled([
    fetchUser(), fetchProduct()
  ])
}

结果对象有 3 个属性:

  • status-"fulfilled"要么"rejected"
  • value- 仅在 status === 'fulfilled'时出现。Promise 被 resolve 的 value
  • reason- 仅在 status === 'rejected' 时出现。Promise 被 reject 的 reson。

所以我们现在可以读取每个承诺的状态,并单独处理每个错误,而不会丢失任何关键信息:

async function getPageData() {
  // 同时触发两个请求
  const [userResult, productResult] = await Promise.allSettled([
    fetchUser(), fetchProduct()
  ])

  //  user
  if (userResult.status === 'rejected') {
    const err = userResult.reason
    handle(err)
  } else {
    const user = userResult.value
  }

  //  product
  if (productResult.status === 'rejected') {
    const err = productResult.reason
    handle(err)
  } else {
    const product = productResult.value
  }
}

但是,这是很多重复代码。那么让我们抽象一下:

async function getPageData() {
  const results = await Promise.allSettled([
    fetchUser(), fetchProduct()
  ])

  // 更美观
  const [user, product] = handleResults(results)
}

我们可以像这样实现一个简单的handleResults功能:

// 如果发生任何错误 则抛出泛型函数,或返回响应
// 如果没有错误发生
function handleResults(results) {
  const errors = results
    .filter(result => result.status === 'rejected')
    .map(result => result.reason)

  if (errors.length) {
    // 将所有错误聚合为一个
    throw new AggregateError(errors)
  }

  return results.map(result => result.value)
}

我们可以在这里使用一个巧妙的技巧,即AggergateError类,来抛出一个可能包含多个内部的错误。这样,当被捕获时,我们会通过.errorsan 上的属性获得包含所有详细信息的单个错误AggregateError,其中包括所有错误:

async function getPageData() {
  const results = await Promise.allSettled([
    fetchUser(), fetchProduct()
  ])

  try {
    const [user, product] = handleResults(results)
  } catch (err) {
    for (const error of err.errors) {
      handle(error)
    }
  }
}

后记

项目实际过程中,如果需要捕获每个请求的错误,可以在axios等响应拦截器中判断响应代码是否正确,这样就可以拦截所有错误的请求。 但是如果想在每个页面单独自定义捕获信息就可以用如上方法。 两者并不冲突

总结

  • 如果Promise.all请求需要捕获每个错误,就用Promise.allSettled
  • 确保我们避免混淆——我想指出,重要的是要注意,当我们在这里谈论并发时,我们指的是并发等待Promise,而不是并发执行代码。
  • 避免过早优化,并确保在增加更多复杂性之前有充分的理由。速度快固然好,但在盲目并发代码中的所有内容之前考虑是否需要它。
  • JavaScript 中的 Promise 非常强大,虽然在过度地将顺序异步/等待转换为并发等待之前应该谨慎行事,但 JavaScript 内置了许多有用的工具,可帮助您在需要时加快速度,这些值得了解。

完全体代码

function handle(err) {
  alertToUser(err)
  saveToLoggingService(err)
}

function handleResults(results) {
  const errors = results
    .filter(result => result.status === 'rejected')
    .map(result => result.reason)

  if (errors.length) { 
    throw new AggregateError(errors)
  }

  return results.map(result => result.value)
}

async function getPageData() {
  const results = await Promise.allSettled([
    fetchUser(), fetchProduct()
  ])

  try {
    const [user, product] = handleResults(results)
  } catch (err) {
    for (const error of err.errors) {
      handle(error)
    }
  }
}


谢谢!