在日常的前端开发工作中,我们经常需要同时处理多个异步任务。想象一下,你正在开发一个仪表盘页面,需要从三个不同的 API 获取数据:用户信息、订单列表和系统通知。如果其中一个 API 挂了,其他的 API 数据还需要展示吗?
通常情况下,如果我们使用 Promise.all(),只要有一个请求失败,整个流程就会中断,我们将无法获得那些成功请求的数据。这在很多实际场景下并不是我们想要的结果。
在这篇文章中,我们将深入探讨一个能够解决这一痛点的强大方法——Promise.allSettled()。我们将学习它如何让我们并行运行多个 Promise,并且无论结果是成功还是失败,都能获取到所有任务的最终反馈。让我们开始吧!
什么是 Promise.allSettled()?
Promise.allSettled() 是 ES2020 引入的一个静态方法,它接受一个 Promise 的 iterable(例如数组)作为输入,并返回一个新的 Promise。这个新的 Promise 会在所有给定的 Promise 都“敲定”后解决。
这里的“敲定”是一个关键术语。在 JavaScript 中,Promise 的状态一旦改变,就不会再变。所谓的敲定,包含了两种情况:
- 兑现:操作成功完成。
- 拒绝:操作失败。
与 INLINECODE65576c1e 不同,INLINECODE8dedfc2c 不会发生“短路”现象。这意味着,即便其中有一个或多个 Promise 被拒绝了,它也会耐心地等待其他 Promise 完成。这对于我们想要获取所有任务完整反馈的场景(比如统计批量操作的成功率,或者上文提到的仪表盘加载)非常有用。
核心特性
在深入代码之前,让我们先总结一下它的核心特性,做到心中有数:
- 并行执行:它可以同时发起多个异步操作,充分利用并发能力。
- 不轻易放弃:无论成员 Promise 是成功还是失败,它都会运行到底。
- 结构化反馈:它返回的不是一个简单的值数组,而是一个包含状态和结果的详细对象数组。
- 结果对象清晰:对于成功的 Promise,我们得到 INLINECODE7f6d2d02;对于失败的 Promise,我们得到 INLINECODE943c0c69。
基本语法
让我们来看看它的语法结构:
Promise.allSettled(iterable);
参数:
这个方法接受一个单一的参数 iterable。通常,我们会传入一个数组,数组中可以包含 Promise 对象,甚至是非 Promise 的值(它们会被自动视为已兑现的 Promise)。
返回值:
它返回一个处于 pending 状态的 Promise。一旦迭代中的所有 Promise 都已完成(兑现或拒绝),返回的 Promise 就会异步解决,并返回一个包含结果对象的数组。
代码示例与实战解析
为了让你更好地理解,我们准备了几个从简单到复杂的实战示例。
#### 示例 1:基础用法与混合状态
在这个例子中,我们创建了一个包含成功、失败和延迟任务的混合场景。让我们看看 Promise.allSettled() 如何优雅地处理它们。
// 1. 创建一个立即成功的 Promise
const p1 = Promise.resolve("Task 1 completed successfully");
// 2. 创建一个立即失败的 Promise
const p2 = Promise.reject(new Error("Task 2 encountered an error"));
// 3. 创建一个延迟成功的 Promise
const p3 = new Promise((resolve) => {
setTimeout(() => resolve("Task 3 completed after delay"), 100);
});
// 使用 allSettled 等待所有任务结束
Promise.allSettled([p1, p2, p3])
.then((results) => {
// results 是一个包含 3 个对象的数组
console.log("All tasks settled. Results:");
results.forEach((result, index) => {
if (result.status === ‘fulfilled‘) {
console.log(`Task ${index + 1} Success:`, result.value);
} else {
console.log(`Task ${index + 1} Failed:`, result.reason.message);
}
});
});
代码解析:
请注意我们是如何检查 INLINECODE9c3192b5 的。INLINECODE45ee9214 确保了我们能够遍历 INLINECODEd338807f 数组,而不用担心抛出未捕获的异常。在上述代码中,即便 INLINECODEd7bd35f9 失败了,INLINECODEf90acb7f 依然会正常执行并返回结果,这是使用 INLINECODE1a08e585 无法做到的。
#### 示例 2:处理非 Promise 值
allSettled 非常包容。如果你传入的数组中包含普通的数字或字符串,它们会被自动视为已兑现的 Promise。
const mixedInputs = [
Promise.resolve(33),
66, // 非 Promise 值
Promise.reject(new Error(‘An error‘))
];
Promise.allSettled(mixedInputs).then((results) => {
results.forEach((result) => {
console.log(result);
});
});
/* 输出预期:
[ { status: ‘fulfilled‘, value: 33 },
{ status: ‘fulfilled‘, value: 66 },
{ status: ‘rejected‘, reason: Error: An error } ]
*/
#### 示例 3:模拟真实的批量数据请求
让我们构建一个更真实的场景。假设我们需要上传多个文件到服务器,其中一些可能会因为网络问题失败。我们希望告诉用户哪些上传成功了,哪些失败了,而不是一旦有一个失败就停止所有上传。
// 模拟文件上传函数
function simulateFileUpload(fileName, shouldFail) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(`${fileName} 上传失败: 网络超时`);
} else {
resolve(`${fileName} 上传成功`);
}
}, Math.random() * 1000);
});
}
const files = [
{ name: ‘profile.jpg‘, fail: false },
{ name: ‘document.pdf‘, fail: true }, // 这个会失败
{ name: ‘avatar.png‘, fail: false }
];
// 将文件对象映射为上传 Promise
const uploadPromises = files.map(file =>
simulateFileUpload(file.name, file.fail)
);
console.log("开始批量上传...");
Promise.allSettled(uploadPromises).then((results) => {
console.log("批量上传处理完成。");
let successCount = 0;
let failureCount = 0;
results.forEach((result, i) => {
if (result.status === ‘fulfilled‘) {
console.log(`✅ ${result.value}`);
successCount++;
} else {
console.log(`❌ ${result.reason}`);
failureCount++;
}
});
console.log(`最终统计: 成功 ${successCount}, 失败 ${failureCount}`);
});
实用见解:
这种模式在处理批量操作时非常强大。你不再需要手动维护计数器来追踪有多少个 Promise 完成了,allSettled 会帮你处理所有的等待和结果收集工作。
#### 示例 4:对比 Promise.all() 的行为差异
为了让你更深刻地理解“不短路”的特性,我们可以对比一下 INLINECODEa8e469dc 和 INLINECODEa5b62a02。
const task1 = Promise.resolve("OK");
const task2 = Promise.reject("Fail");
const task3 = Promise.resolve("OK also");
// 使用 Promise.all - 遇到拒绝立即停止
Promise.all([task1, task2, task3])
.then(() => console.log("Promise.all: 所有都成功了"))
.catch((err) => console.log("Promise.all: 捕获到错误 ->", err));
// 使用 Promise.allSettled - 执行到底
Promise.allSettled([task1, task2, task3])
.then(() => console.log("Promise.allSettled: 全部执行完毕,结果已整理"));
在 INLINECODE005bbc5b 中,如果 INLINECODEec84193c 失败,INLINECODEf6f61142 回调根本不会执行,直接跳到 INLINECODE2da08fbd。而在 INLINECODE202b9194 中,INLINECODE90044383 始终会执行,给你处理每一个结果的机会。
进阶应用与最佳实践
既然我们已经掌握了基本用法,让我们探讨一些在实际开发中如何更好地使用它。
#### 1. 空数组的边界情况
如果你传入一个空数组 INLINECODE07f0f978,它会返回一个状态为 INLINECODEf821775a 的 Promise,结果是一个空数组。这在动态构建任务列表时非常有用,因为你不需要额外写逻辑去判断“数组是否为空”。
Promise.allSettled([]).then(results => {
console.log(results); // []
console.log("空数组也被成功处理了");
});
#### 2. Polyfill 与兼容性
虽然现代浏览器都已经支持 INLINECODE5911ae65,但如果你需要支持非常老的浏览器(比如 IE),你可能需要一个 Polyfill。其核心逻辑其实就是使用 INLINECODE0280576a 配合 INLINECODE90ab1964 和 INLINECODE8ad04bd6 来将每个 Promise 转换为总是 resolve 的形式。
// 简单的 Polyfill 思路示例
if (!Promise.allSettled) {
Promise.allSettled = function(promises) {
return Promise.all(promises.map(p => Promise.resolve(p)
.then(
value => ({ status: ‘fulfilled‘, value }),
reason => ({ status: ‘rejected‘, reason })
)
));
};
}
#### 3. 错误处理策略
使用 INLINECODE5ceccb55 时,我们通常不在主链上使用 INLINECODE5aef7cf6,因为拒绝被视为一种“结果”而不是“异常”。你应该在 INLINECODE4ccc4f95 回调内部,通过检查 INLINECODE228d0479 来决定如何处理错误。
常见错误与解决方案
错误 1:混淆 INLINECODE3a304d97 和 INLINECODEd4393563 属性
新手常犯的错误是直接访问 INLINECODE5ff5aa2d 而不检查状态。如果 Promise 被拒绝了,它就没有 INLINECODE91089031 属性,只有 INLINECODEe65183c3 属性。直接访问会导致 INLINECODE57d15fb3。
解决方案: 始终先检查 result.status。
错误 2:忽视性能开销
虽然它很方便,但如果你的任务列表非常庞大(例如循环遍历数据库中的 10,000 条记录并创建 Promise),一次性并发可能会导致浏览器卡顿或服务器压力过大(DDoS 自己的 API)。
解决方案: 对于大量任务,考虑使用“并发控制池”,即分批执行 Promise,而不是一次性全部 allSettled。
性能优化建议
在处理大量异步任务时,单纯地使用 Promise.allSettled([p1, p2, ... p1000]) 并不是最优解。浏览器对同时打开的网络连接数是有限制的。
你可以实现一个分批处理函数,例如,每次只处理 5 个 Promise,当这 5 个全部 settled 后,再处理下一批。这能显著降低资源峰值消耗。
总结
在今天的文章中,我们深入探讨了 Promise.allSettled()。它是处理复杂异步逻辑的一把瑞士军刀,特别是在我们需要“尽最大努力”完成所有任务的场景下。
让我们回顾一下关键点:
- 它接受一个 iterable 并返回一个新的 Promise。
- 它会等待所有 Promise 敲定(成功或失败)。
- 它不会因为中途的失败而短路停止。
- 它返回的对象包含 INLINECODE628602f2 (INLINECODEaac8e4bc 或 INLINECODE01da7aca) 以及对应的 INLINECODEa8e7700a 或
reason。
你的下一步行动:
下次当你发现自己正在写 INLINECODEc8a2c11d 并且不得不为其添加 INLINECODE44d358e7 来防止整个流程崩溃,或者发现自己需要手动计数来判断所有任务是否完成时,不妨停下来,试着使用 Promise.allSettled() 重构你的代码。它会让你的代码更加健壮、易于维护且逻辑更加清晰。
希望这篇文章能帮助你更好地掌握 JavaScript 的异步编程!祝编码愉快!