本文旨在描述使用 Promise.all() 时经常出现的棘手情况,以及该问题的简单解决方案。 Promise.all() 的 MDN 页面提供了该函数的以下描述: Promise.all() 静态方法采用可迭代的 Promise 作为输入……
本文旨在描述使用时经常出现的棘手情况 Promise.all()
,以及解决此问题的简单方法。
上下文
MDN 页面 Promise.all()
提供了该函数的以下描述:
这
Promise.all()
静态方法接受可迭代的 Promise 作为输入并返回一个 承诺。 当所有输入的 Promise 都满足时(包括传递空的可迭代对象时),返回的 Promise 就会满足,并带有一个满足值数组。 当任何输入的 Promise 拒绝时,它会拒绝,并给出第一个拒绝原因。
Promise.all()
于 2015 年发布的 ECMAScript 第六版中引入,当 Promise 首次作为语言中的内置机制添加时,但它们之前已经通过使用像 蓝鸟。
ECMAScript 2020,第11版,推出 Promise.allSettled()
,其行为略有不同:
这
Promise.allSettled()
静态方法接受可迭代的 Promise 作为输入并返回一个 承诺。 当所有输入的承诺都解决时(包括传递空的可迭代对象时),返回的承诺就会履行,并带有描述每个承诺结果的对象数组。
最后,随着 async
, Promise.all()
& Promise.allSettled()
可以用来 await
用于完成几个异步功能:
await Promise.all([asyncFunc1(), asyncFunc2()]) // can throw an exception
const results = await Promise.allSettled([asyncFunc1(), asyncFunc2()]) // never throw an exception
问题
Promise.all()
是一个非常方便的函数,但它在错误管理方面的行为实际上相当棘手。
然而 Promise.allSettled()
需要检查承诺的结果状态,产生的承诺 Promise.all()
将要 拒绝 如果任何输入承诺失败。 在这种情况下,这意味着 await Promise.all(...)
会抛出异常。
但 什么时候 Promise.all()
拒绝,一些输入的 Promise 仍然可以执行!
而这种情况往往是开发者没有考虑到的。
下面的图表和代码片段演示了这个问题:
async function sleep(durationMs) {
return new Promise((resolve) => setTimeout(resolve, durationMs));
}
async function sleepAndFail(durationMs) {
await sleep(durationMs);
throw new Error(`sleepAndFail(${durationMs}) FAILED`);
};
let promiseStatus = "not-started";
async function sleepAndTrackStatus(durationMs) {
promiseStatus = "executing";
await sleep(durationMs);
promiseStatus = `sleepAndTrackStatus(${durationMs}) SUCCEEDED`;
};
(async function () {
const failing = sleepAndFail(100);
const fastSucceeding = sleep(50);
const slowSucceeding = sleepAndTrackStatus(200);
try {
console.log(await Promise.all([
failing,
fastSucceeding,
slowSucceeding,
]));
} catch (error) {
console.log(error); // -> Error: sleepAndFail(100) FAILED
}
console.log("Final promiseStatus for slowSucceeding:", promiseStatus); // -> executing!
})()
问题是,当输入承诺失败时, Promise.all()
将要 尽早拒绝,无需等待其他承诺,仍然可以异步处理。
这可能会导致许多有问题的情况,其中其他 Promise 执行的代码执行的操作可能与调用后执行的代码发生冲突 Promise.all()
。 例如,数据库连接或文件 I/O 问题是可以预料的。
就我而言,我在级联故障的情况下发现了这个根本问题 是 测试套件:一些异步代码 afterEach()
当某些单元测试由于调用而失败时,方法没有正确等待共享资源被清理 await Promise.all()
。
根本原因
出于好奇,我试图找出实现的源代码 Promise.all()
。 我认为它的大部分逻辑都在文件中 src/builtins/promise-all.tq 在 V8 JavaScript 引擎中。
.tq
文件写入 扭矩,一种特定于 V8 的高级语言,可转换为 C++。 因此,我发现很难弄清楚“短路”承诺拒绝逻辑在实现中到底发生在哪里。
对于好奇的读者,优秀的文章中有一个关于 Torque 语言的部分 Learning-v8 存储库,作者:Daniel Bevenius。
一个办法
对于这个问题, Promise.allSettled()
是一个更好的替代品 Promise.all()
,因为它等待作为参数传递给该函数的所有承诺的完成。 实际上, allSettled
正是由于这个原因,EcmaScript 被引入,因为 其原始提案文件 揭示了:
Promise.allSettled
其独特之处在于始终等待其所有输入值。
然而, Promise.allSettled()
要求您自己处理潜在的拒绝,这很容易被忘记,或者创建重复的样板代码。
我建议的解决方案是 一个简单的安全替代品 Promise.all()
:
/*
* This function ensures that (1) all the promises provided have completed
* and (2) that a rejection is produced if at least one of those promises is rejected.
*/
async function waitForPromises<T>(promises: Iterable<PromiseLike<T>>) {
const results = await Promise.allSettled(promises);
const rejectedResults: PromiseRejectedResult[] = results.filter(
(result): result is PromiseRejectedResult => result.status === "rejected"
);
if (rejectedResults.length === 1) {
throw rejectedResults[0].reason;
}
if (rejectedResults.length > 1) {
throw new AggregateError(rejectedResults.map((result) => result.reason), `${rejectedResults.length} promises failed`);
}
const successfullResults: PromiseFulfilledResult<Awaited<T>>[] = results.filter(
(result): result is PromiseFulfilledResult<Awaited<T>> => result.status === "fulfilled"
);
return successfullResults.map((result) => result.value);
}
这其实是 带有泛型的 TypeScript,但你可以删除 : types
和
获取有效的 Javascript 代码:
JavaScript 中的 waitForPromises
async function waitForPromises(promises) {
const results = await Promise.allSettled(promises);
const rejectedResults = results.filter(result => result.status === "rejected");
if (rejectedResults.length === 1) {
throw rejectedResults[0].reason;
}
if (rejectedResults.length > 1) {
throw new AggregateError(rejectedResults.map((result) => result.reason), `${rejectedResults.length} promises failed`);
}
return results.map((result) => result.value);
}
您可以通过替换来测试此功能 Promise.all
经过 waitForPromises
在本文的最初代码片段中。
虽然有时会出现“短路”行为 Promise.all
可以方便,我认为 waitForPromises
在大多数情况下是更好、更安全的选择,并且应该是首选的默认选项 await
完成几个异步功能。
(谢谢 Reddit 用户@senoptic 有关此博文的非常相关的反馈)