JavaScript Promise - 批量处理 (3/6)
有时候我们可能需要同时监控多个 promise 的处理进度,以确定下一步的行为。JavaScript 提供了几个满足这些需求的方法,本篇主要看一下这些方法之间的区别。
Promise.all()
方法
概念
- 接受一个参数,这个参数是一组可迭代的 promise (如 promise 数组)
- 当这组 promise 都成功时,
Promise.all()
返回一个成功状态的、包含一组值的 promise - 如果有任意一个 promise 失败,则
Promise.all()
立即、马上、毫不犹豫地返回,不会等待其它未完成的 promise (但其它 promise 仍会继续执行完毕),且返回一个失败状态的、包含单个值的 promise
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()
,进而被转换为一个 promisePromise.all()
若成功,它的结果是一组值,且值的顺序和其参数的 promise 一一对应
如果 Promise.all()
失败,则其结果是失败的那个 promise 的返回值:
// 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 时,有时候我们需要将多个文件的内容合并处理,这种情况下我们可以并行读取多个文件,等所有文件读取完毕之后再进行下一步操作:
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 获取数据并聚合结果以供使用。
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()
故意增加一个延迟。
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 失败原因
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 动画,我们希望等待所有的动画完成,但并不在乎是否有一两个失败:
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"))
Promise.any()
方法
概念
Promise.any()
和 Promise.all()
相反:后者是有任意一个 promise 失败就立即返回;前者是有任意一个 promise 成功就立即返回。
但如果参数中的所有 promise 都失败了,将返回一个带有 AggregateError
的 promise,参数中的 promise 的结果都保存在返回值的 errors
属性中。
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)
对冲请求即客户端同时向多个服务端发起请求,接受首先返回的响应。在客户端需要尽可能低的延迟,且有服务器资源专门用于管理额外负载和重复响应的情况下,这很有用。
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()
来选择更快的响应。
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()
也是成功状态。
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()
设置超时。
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 也会继续执行,但其结果大概也许真的是被丢弃了。