JavaScript Promise - 批量处理 (3/6)

有时候我们可能需要同时监控多个 promise 的处理进度,以确定下一步的行为。JavaScript 提供了几个满足这些需求的方法,本篇主要看一下这些方法之间的区别。

Promise.all() 方法

概念

  • 接受一个参数,这个参数是一组可迭代的 promise (如 promise 数组)
  • 当这组 promise 都成功时,Promise.all() 返回一个成功状态的、包含一组值的 promise
  • 如果有任意一个 promise 失败,则 Promise.all() 立即、马上、毫不犹豫地返回,不会等待其它未完成的 promise (但其它 promise 仍会继续执行完毕),且返回一个失败状态的、包含单个值的 promise
js 复制代码
const p1 = Promise.resolve(2)
const p2 = new Promise((resolve, reject) => resolve(3))
const p3 = new Promise((resolve, reject) => setTimeout(() => resolve(4), 100))

const p4 = Promise.all([p1, p2, p3, 5])
p4.then(v => {
  console.log(Array.isArray(v)) // true
  console.log(v) // [ 2, 3, 4, 5 ]
})

上面的代码说明两点:

  • Promise.all() 的参数中的任何非 promise 的元素 (如上面的 5) 都会被隐式传递给 Promise.resolve(),进而被转换为一个 promise
  • Promise.all() 若成功,它的结果是一组值,且值的顺序和其参数的 promise 一一对应

如果 Promise.all() 失败,则其结果是失败的那个 promise 的返回值:

js 复制代码
// promise-all.mjs
const p1 = Promise.resolve(2)
const p2 = new Promise((resolve, reject) => reject(3))
const p3 = new Promise((resolve, reject) => setTimeout(() => resolve(4), 5000))

const p4 = Promise.all([p1, p2, p3, 5])
p4.catch(v => {
  console.log(Array.isArray(v)) // false
  console.error(v) // 3
})

注意,用 node promise-all.mjs 执行上面的代码,会立刻输出 false 和 3 两个值,然后停顿 5 秒等待 p3 执行完毕。

用例

同时处理多个文件

尤其是在使用服务端 JavaScript 运行时如 Node.js 时,有时候我们需要将多个文件的内容合并处理,这种情况下我们可以并行读取多个文件,等所有文件读取完毕之后再进行下一步操作:

js 复制代码
import { readFile } from "node:fs/promises"

function readFiles(filenames) {
  return Promise.all(filenames.map((filename) => readFile(filename, "utf8")))
}

readFiles(["file1.json", "file2.json"])
  .then((contents) => {
    const data = contents.map((c) => JSON.parse(c))
    console.log(data)
  })
  .catch((reason) => console.error(reason.message))
调用多个依赖的 API

另一个常见的例子是我们需要从多个 API 获取数据并聚合结果以供使用。

js 复制代码
const API_BASE = "https://jsonplaceholder.typicode.com"

function createError(response) {
  return new Error(
    `Unexpected: ${response.status} ${response.statusText} for ${response.url}`
  )
}

function fetchUserData(userId) {
  const urls = [
    `${API_BASE}/users/${userId}/posts`,
    `${API_BASE}/users/${userId}/albums`,
  ]

  return Promise.all(urls.map((url) => fetch(url)));
}

fetchUserData(1)
  .then((responses) => {
    return Promise.all(
      responses.map((response) => {
        if (response.ok) {
          return response.json()
        } else {
          return Promise.reject(createError(response))
        }
      })
    )
  })
  .then(([posts, albums]) => {
    console.log(posts)
    console.log("\n\n")
    console.log(albums)
  })
  .catch((reason) => console.error(reason.message))
故意增加延迟

一个不常见的例子是使用 Promise.all() 故意增加延迟,这大多发生在客户端而非服务端。假设我们有一个酷炫的 loading 控件,为了防止 API 响应太快来不及展示这个酷炫,我们可以利用 Promise.all() 故意增加一个延迟。

js 复制代码
const API_BASE = "https://jsonplaceholder.typicode.com"
const appElement = document.getElementById("app")

function delay(milliseconds) {
  return new Promise((resolve, reject) =>
      // 此处 resolve() 不必有值
    setTimeout(() => resolve(), milliseconds)
  )
}

function fetchUserData(userId) {
  appElement.classList.add("loading")

  const urls = [
    `${API_BASE}/users/${userId}/posts`,
    `${API_BASE}/users/${userId}/albums`,
  ]

  return Promise.all([...urls.map((url) => fetch(url)), delay(1500)]).then(
    (results) => {
        // 移除 delay promise 的结果
      return results.slice(0, results.length - 1)
    }
  )
}

fetchUserData(1).then(
    // ...
).then(
    // ...
).finally(() => appElement.classList.remove("loading")).catch(
    // ...
)

Promise.allSettled() 方法

概念

顾名思义,Promise.allSettled() 等待所有的 promise "all settled",即等待参数中的所有 promise 都执行完毕,无论成功与否。因此这个方法总是返回一个成功的 promise,值是参数的结果组成的对象数组。

这个对象数组中,成功的对象有两个属性:

  • status: 值总是 fulfilled
  • value: 值是对应参数中的 promise 结果

失败的对象有两个属性:

  • status: 值总是 rejected
  • reason: 值是对应参数中的 promise 失败原因
js 复制代码
const p1 = Promise.resolve(2)
const p2 = Promise.reject(3)
const p3 = new Promise((resolve, reject) => setTimeout(() => resolve(4), 100))

const p4 = Promise.allSettled([p1, p2, p3])
p4.then((results) => {
  console.log(Array.isArray(results)) // true

  console.log(results[0].status) // fulfilled
  console.log(results[0].value) // 2

  console.log(results[1].status) // rejected
  console.log(results[1].reason) // 3

  console.log(results[2].status) // fulfilled
  console.log(results[2].value) // 4
})

p4 总是一个成功的 promise,所以调用 then() 方法。

用例

Promise.allSettled() 最适合允许部分 promise 失败的情况。比如前端有一些 CSS 动画,我们希望等待所有的动画完成,但并不在乎是否有一两个失败:

js 复制代码
function waitForAnimations(element) {
  return Promise.allSettled(
      // `getAnimations()` 方法返回一个 animation 对象的数组,每个对象都包含一个
      // `finished` 属性,值为 promise
    element.getAnimations().map((animation) => animation.finished)
  )
}

const toasterElement = document.getElementById("toaster")
waitForAnimations(toasterElement).then(() => console.log("Toaster is done"))

参考:Building a toast component

Promise.any() 方法

概念

Promise.any()Promise.all() 相反:后者是有任意一个 promise 失败就立即返回;前者是有任意一个 promise 成功就立即返回。

但如果参数中的所有 promise 都失败了,将返回一个带有 AggregateError 的 promise,参数中的 promise 的结果都保存在返回值的 errors 属性中。

js 复制代码
const p1 = Promise.reject(2)
const p2 = new Promise((resolve, reject) => reject(3))
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => reject(4), 100)
})

Promise.any([p1, p2, p3]).catch((reason) => {
  // 运行时报错
  console.error(reason.message) // All promises were rejected

  // 各个 promise 的报错返回值
  console.error(reason.errors[0]) // 2
  console.error(reason.errors[1]) // 3
  console.error(reason.errors[2]) // 4
})

用例

执行对冲请求 (hedged requests)

对冲请求即客户端同时向多个服务端发起请求,接受首先返回的响应。在客户端需要尽可能低的延迟,且有服务器资源专门用于管理额外负载和重复响应的情况下,这很有用。

js 复制代码
const HOSTS = ["api1.example.com", "api2.example.com"]

function hedgedFetch(endpoint) {
  return Promise.any(HOSTS.map((host) => fetch(`https://${host}${endpoint}`)))
}

hedgedFetch("/transactions").then().catch()
在 Service Worker 中使用最快的响应

使用 Service Worker 的 Web 页面通常可以选择从哪里加载数据:从网络还是从缓存中。在某些情况下,网络请求可能比从缓存加载更快,因此可以使用 Promise.any() 来选择更快的响应。

js 复制代码
self.addEventListener("fetch", (event) => {
  const cachedResponse = caches.match(event.request)
  const fetchedResponse = fetch(event.request.url)

  event.respondWith(
    Promise.any([fetchResponse.catch(() => cachedResponse), cachedResponse])
      .then((response) => response ?? fetchedResponse)
      .catch(() => {})
  )
})
  • fetch event listener 是我们可以监听网络请求并截取响应
  • caches.match() 返回一个总是成功的 promise,但其值要么是响应体,要么是 undefined (缓存未命中)
  • event.respondWith() 接受一个 promise 作为参数,此处即返回 Promise.any() 的结果

Promise.race() 方法

概念

Promise.race() 接受一组 promise 作为参数,如果有任意一个 promise 执行完毕,无论成功与否,Promise.race() 都使用其结果返回;如果首先返回的 promise 是失败状态,Promise.race() 也是失败状态;如果首先返回的 promise 是成功状态,Promise.race() 也是成功状态。

js 复制代码
let p1 = Promise.resolve(2)
let p2 = new Promise((resolve, reject) => resolve(3))
let p3 = new Promise((resolve, reject) => setTimeout(() => resolve(4), 100))

// p1 在创建时即为成功状态,不难理解它第一个返回
Promise.race([p1, p2, p3]).then(v => console.log(v)) // 2

p1 = new Promise((resolve, reject) => setTimeout(() => resolve(10), 100))
p2 = new Promise((resolve, reject) => reject(20))
p3 = new Promise((resolve, reject) => setTimeout(() => resolve(10), 50))

Promise.race([p1, p2, p3]).catch(v => console.error(v)) // 20

用例

比如 fetch() 方法的请求没有超时,它会挂起,直到以某种方式完成。我们可以使用 Promise.race()fetch() 设置超时。

js 复制代码
function timeout(milliseconds) {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error("Request timeout.")), milliseconds)
  })
}

function fetchWithTimeout(...args) {
  return Promise.race([fetch(...args), timeout(1000)])
}

const API_URL = "https://jsonplaceholder.typicode.com/users"

fetchWithTimeout(API_URL)
  .then((response) => response.json())
  .then((users) => console.log(users))
  .catch((reason) => console.error(reason.message))

总结

  • Promise.all():任一 promise 失败即立刻返回
  • Promise.allSettled():等待所有 promise 完成
  • Promise.any():任一 promise 成功即立刻返回
  • Promise.race():任一 promise 完成即立刻返回

有一点需要注意的是:这些方法中的 promise 都会执行完毕,即使方法已经返回结果,未执行完毕的 promise 也会继续执行,但其结果大概也许真的是被丢弃了。

相关推荐
恋猫de小郭22 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端