JavaScript 中的 Promise 是帮助我们执行异步操作的强大的 API 之一。

Promise.all 将异步操作提升到新的水平,因为它可以帮助你汇总一组 Promise。换句话说,我可以说它帮助你进行并发操作(有时是免费的)。

前提:

你需要知道 JavaScript 中的 Promise 是什么。

什么是 Promise.all

Promise.all 实际上是一个 promise,它将一组 promise 作为输入(可迭代)。然后,当所有 promise 都执行或其中任何一个执行失败时,它就会执行。

例如,假设你有十个 promise(执行网络调用或数据库连接的异步操作),你必须知道所有 promise 何时执行,或者必须等到所有 promise 执行。因此,你正在将所有十个 promise 传递给 Promise.all。然后,当所有十个 promise 都执行,或者十个 promise 中的任何一个因错误而执行失败,Promise.all 本身作为一个 promise 将执行。

我们来看看代码

Promise.all([Promise1, Promise2, Promise3])
 .then(result) => {
   console.log(result)
 })
 .catch(error => console.log(`Error in promises ${error}`))

如你所见,我们正在将数组传递给 Promise.all。当所有三个 promise 都执行后,Promise.all 就执行了,并且打印输出。

我们看看例子

// 一个简单的 promise,在给定时间后执行
const timeOut = (t) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Completed in ${t}`)
    }, t)
  })
}

// 执行一个正常的 promise
timeOut(1000)
 .then(result => console.log(result)) // 在 1000ms 内完成执行

// Promise.all
Promise.all([timeOut(1000), timeOut(2000)])
 .then(result => console.log(result)) // ["在 1000ms 内完成执行", "在 2000ms 内完成执行"]

在上面的示例中,Promise.all 在 2000ms 之后执行,并且将输出作为数组打印出来。

关于 Promise.all 的一件有趣的事情是,promise 的顺序不变,意思是,数组中的第一个 promise 执行后将是输出数组的第一个元素,第二个 promise 对应输出数组中的第二个元素,依此类推。

我们看看另一个例子

// 一个简单的 promise,在给定时间后执行
const timeOut = (t) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Completed in ${t}`)
    }, t)
  })
}

const durations = [1000, 2000, 3000]

const promises = []

durations.map((duration) => {
  // 下一行中,发生了两件事
  // 1. 调用异步函数(timeout())。现在,异步函数开始执行,进入 'pending' 状态。
  // 2. 将 pending promise 放到一个数组中。
  promises.push(timeOut(duration)) 
})

console.log(promises) // [ Promise { "pending" }, Promise { "pending" }, Promise { "pending" } ]

// 将 pending promises 的数组传递给 Promise.all
// Promise.all 将等待所有 promise 执行后,才会执行。
Promise.all(promises)
.then(response => console.log(response)) // ["在 1000ms 内完成执行", "在 2000ms 内完成执行", "在 3000ms 内完成执行"]

从上面的示例可以看出,Promise.all 一直等到所有的 promise 都执行了才执行。

我们看看如果任何一项 promise 执行失败,会发生什么。

// 一个简单的 promise,在给定时间后执行
const timeOut = (t) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (t === 2000) {
        reject(`Rejected in ${t}`)
      } else {
        resolve(`Completed in ${t}`)
      }
    }, t)
  })
}

const durations = [1000, 2000, 3000]

const promises = []

durations.map((duration) => {
  promises.push(timeOut(duration)) 
})

// 将 pending promises 的数组传递给 Promise.all
Promise.all(promises)
.then(response => console.log(response)) // Promise.all 不能执行,因为传递过来的其中一个 promise 执行失败。
.catch(error => console.log(`Error in executing ${error}`)) // Promise.all 抛出错误。

如你所见,如果一个 promise 执行失败,那么所有其他 promise 都会执行失败,然后 Promise.all 执行失败。

在某些用例中,你不想发生上面这种情况。即使某些 promise 执行失败,你也需要执行所有的 promise,或者你可以稍后处理执行失败的 promise。

让我们来看看如何处理。

const durations = [1000, 2000, 3000]

promises = durations.map((duration) => {
  return timeOut(duration).catch(e => e) // 处理每个 promise 的错误。
})

Promise.all(promises)
  .then(response => console.log(response)) // ["在 1000ms 内完成执行", "在 2000ms 内执行失败", "在 3000ms 内完成执行"]
  .catch(error => console.log(`Error in executing ${error}`))
view raw

Promise.all 用例

假设你必须执行大量的异步操作,例如向成千上万的用户发送批量营销电子邮件。

简单的伪代码为:

for (let i=0;i<50000; i += 1) {
 sendMailForUser(user[i]) // 异步操作发送一封邮件
}

上面的例子很简单,但是效果不是很好。堆栈将变得太沉重,并且在某个时间点,JavaScript 将具有大量开放的 HTTP 连接,这可能会杀死服务器。

一种简单的执行方法是分批执行,先接收 500 个用户,触发邮件,然后等到所有 HTTP 连接都关闭,然后处理下一批,以此类推。

让我们来看一个例子:

// 异步操作发送邮件给一些用户。
const sendMailForUsers = async (users) => {
  const usersLength = users.length
  
  for (let i = 0; i < usersLength; i += 100) { 
    const requests = users.slice(i, i + 100).map((user) => { // 每批次发送邮件给 100 个用户。
      return triggerMailForUser(user) // 异步操作发送邮件。
       .catch(e => console.log(`Error in sending email for ${user} - ${e}`)) // 如果有什么问题,则会识别错误,这样就不会停止循环。
    })
    
    // 请求会有 100 个或更少的 pending promises。
    // Promise.all 会在所有 promise 执行之后再执行,发送邮件给下一个批次的 100 位用户。
    await Promise.all(requests)
     .catch(e => console.log(`Error in sending email for the batch ${i} - ${e}`)) // 发现错误。
  }
}


sendMailForUsers(userLists)

我们考虑另一种情况:你必须构建一个 API,该 API 可以从多个第三方 API 获取信息,并汇总来自这些 API 的所有响应。

Promise.all 是实现这个的完美方法。我们来看看如何实现。

// 获取用户 GitHub 信息的函数。
const fetchGithubInfo = async (url) => {
  console.log(`Fetching ${url}`)
  const githubInfo = await axios(url) // API call 用于获取用户 GitHub 信息
  return {
    name: githubInfo.data.name,
    bio: githubInfo.data.bio,
    repos: githubInfo.data.public_repos
  }
}

// 迭代所有用户,并返回他们的 Github 信息。
const fetchUserInfo = async (names) => {
  const requests = names.map((name) => {
    const url = `https://api.github.com/users/${name}`
    return fetchGithubInfo(url) // 异步函数,获取用户信息
     .then((a) => {
      return a // 返回用户信息
      })
  })
  return Promise.all(requests) // 等待所有请求被执行
}


fetchUserInfo(['sindresorhus', 'yyx990803', 'gaearon'])
 .then(a => console.log(JSON.stringify(a)))

/*
Output:
[{
  "name": "Sindre Sorhus",
  "bio": "Full-Time Open-Sourcerer ·· Maker ·· Into Swift and Node.js ",
  "repos": 996
}, {
  "name": "Evan You",
  "bio": "Creator of @vuejs, previously @meteor & @google",
  "repos": 151
}, {
  "name": "Dan Abramov",
  "bio": "Working on @reactjs. Co-author of Redux and Create React App. Building tools for humans.",
  "repos": 232
}]
*/

总而言之,Promise.all 是将一组 promise 聚合为一个 promise 的最佳方法。这是在 JavaScript 中实现并发的方法之一。

希望你喜欢这篇文章,并将文章分享给更多人。

原文:All you need to know about Promise.all,作者:Srebalaji Thirumalai