各位编程专家、异步编程爱好者们,大家好!
在现代Web开发中,异步操作无处不在。从网络请求到文件读写,再到定时任务,我们几乎时刻都在与Promise打交道。Promise.all无疑是我们最常用的Promise组合器之一,它能并行执行多个Promise,并在所有Promise都成功时返回一个结果数组,或在任何一个Promise失败时立即拒绝。然而,这种“全有或全无”的模式,在许多复杂的真实世界场景中,显得过于严格。
设想一下,你正在构建一个仪表盘应用,它需要从多个不同的服务获取数据来填充不同的组件。如果其中一个服务暂时不可用,你是否希望整个仪表盘都崩溃,还是希望那些能成功加载的组件依然能正常显示?再比如,你需要从多个镜像服务器下载同一个资源,你只关心哪个服务器能最快地响应并提供数据。如果所有镜像都尝试失败了,你才需要知道这个情况。
这些场景正是 Promise.allSettled 和 Promise.any 这两个强大的Promise组合器大放异彩的地方。它们是ES2020引入的,旨在弥补 Promise.all 和 Promise.race 在特定边缘情况下的不足,为我们处理并发异步任务提供了更精细、更灵活的控制。
今天,我们将深入探讨 Promise.allSettled 和 Promise.any 的工作原理、核心特性,以及它们在各种应用场景,特别是那些棘手的边缘情况下的应用。我们将通过丰富的代码示例,理解如何利用它们构建更加健壮、用户体验更佳的异步系统。
Promise 基础回顾与 Promise.all 的局限性
在深入 Promise.allSettled 和 Promise.any 之前,我们先快速回顾一下Promise的基本状态和 Promise.all 的行为。
一个Promise有三种状态:
- pending (待定):初始状态,既没有成功,也没有失败。
- fulfilled (已成功):操作成功完成。
- rejected (已失败):操作失败。
Promise的状态一旦从 pending 变为 fulfilled 或 rejected,就称为 settled (已敲定),并且状态不可逆转。
Promise.all 是我们最熟悉的组合器之一。它接收一个Promise的可迭代对象(例如数组),并返回一个新的Promise。
- 当所有传入的Promise都成功(fulfilled)时,
Promise.all返回的Promise也会成功,其结果是一个数组,包含所有传入Promise的成功值,顺序与传入Promise的顺序一致。 - 当任何一个传入的Promise失败(rejected)时,
Promise.all返回的Promise会立即失败,其拒绝原因就是第一个失败Promise的拒绝原因。
这种“全成功才成功,一失败就失败”的行为,我们称之为“fail-fast (快速失败)”。
代码示例:Promise.all 的快速失败
function simulateTask(name, success, delay) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (success) {
console.log(`任务 ${name} 成功完成。`);
resolve(`数据-${name}`);
} else {
console.error(`任务 ${name} 失败!`);
reject(new Error(`任务 ${name} 失败原因`));
}
}, delay);
});
}
const taskA = simulateTask('A', true, 1000);
const taskB = simulateTask('B', true, 2000);
const taskC = simulateTask('C', false, 500); // 这个任务会失败
console.log("--- 启动 Promise.all 示例 ---");
Promise.all([taskA, taskB, taskC])
.then(results => {
console.log("Promise.all 所有任务成功:", results);
})
.catch(error => {
console.error("Promise.all 捕获到错误:", error.message);
// 注意:即使 taskA 和 taskB 最终会成功,一旦 taskC 失败,Promise.all 就会立即拒绝
// 并且我们无法直接获取 taskA 和 taskB 的结果
})
.finally(() => {
console.log("--- Promise.all 示例结束 ---");
});
// 预期输出:
// --- 启动 Promise.all 示例 ---
// (500ms 后) 任务 C 失败!
// (大约 500ms 后) Promise.all 捕获到错误: 任务 C 失败原因
// (大约 500ms 后) --- Promise.all 示例结束 ---
// (1000ms 后) 任务 A 成功完成。
// (2000ms 后) 任务 B 成功完成。
// 注意:taskA和taskB的成功消息仍然会打印,但它们的结果不会被 Promise.all 传递给 .then 块。
从上面的例子可以看出,尽管 taskA 和 taskB 最终会成功,但 taskC 的快速失败导致整个 Promise.all 链条中断,我们无法得知 taskA 和 taskB 的执行情况,也无法获取它们的结果。
这种“fail-fast”行为在某些场景下是合理的,例如:
- 强一致性要求:所有数据必须全部获取成功才能进行下一步操作。
- 原子性操作:一组操作必须全部成功才能视为完成,否则全部回滚。
然而,在更多场景下,我们可能需要:
- 无论成功或失败,都想知道所有任务的最终状态和结果。
- 只要有任何一个任务成功,就立即处理,而不需要等待所有任务。
- 即使部分任务失败,也希望继续处理那些成功的任务。
为了解决这些需求,ES2020引入了 Promise.allSettled 和 Promise.any。
Promise.allSettled 深度解析
Promise.allSettled 方法返回一个在所有给定的Promise都已经settled(即fulfilled或rejected)后解析的Promise。它总是解析,而不是拒绝。解析值是一个对象数组,每个对象都描述了相应Promise的结果。
返回值结构
Promise.allSettled 返回的Promise总是解析为一个数组。数组中的每个元素都是一个对象,表示原始Promise数组中对应Promise的最终状态。每个对象包含两个主要属性:
status:一个字符串,表示Promise的最终状态。'fulfilled':表示Promise已成功。'rejected':表示Promise已失败。
- 如果
status是'fulfilled',则对象还会有一个value属性,包含Promise的成功值。 - 如果
status是'rejected',则对象还会有一个reason属性,包含Promise的拒绝原因。
核心特性总结:
- 永不拒绝:
Promise.allSettled返回的Promise永远不会拒绝。它总会等待所有传入的Promise都到达settled状态,然后解析为一个包含所有结果的数组。 - 提供完整信息:它会为每个Promise提供详细的
status、value或reason。
核心应用场景
-
收集所有任务结果,无论成功或失败
这是Promise.allSettled最直接也最重要的用途。当你需要执行一组独立的异步任务,并且关心每个任务的最终结果,无论它们成功与否时,Promise.allSettled是完美的选择。场景示例:一个后台管理系统需要同时向多个微服务发送数据更新请求。即使其中一些服务暂时不可用或返回错误,你仍然希望知道所有请求的最终结果,以便记录日志、显示给管理员,或触发后续的补偿机制。
代码示例:批量数据更新与结果收集
function sendUpdateRequest(serviceName, data, shouldFail = false) { return new Promise((resolve, reject) => { const delay = Math.random() * 1000 + 500; // 模拟网络延迟 setTimeout(() => { if (shouldFail) { console.error(`更新请求到 ${serviceName} 失败!数据: ${data}`); reject(new Error(`服务 ${serviceName} 不可用或数据无效`)); } else { console.log(`更新请求到 ${serviceName} 成功。数据: ${data}`); resolve({ service: serviceName, status: 'OK', processedData: data }); } }, delay); }); } const updates = [ sendUpdateRequest('UserService', { id: 1, name: 'Alice' }), sendUpdateRequest('ProductService', { productId: 101, price: 99.99 }, true), // 模拟失败 sendUpdateRequest('OrderService', { orderId: 500, status: 'shipped' }), sendUpdateRequest('AnalyticsService', { event: 'user_update', userId: 1 }), sendUpdateRequest('NotificationService', { userId: 1, message: 'Your order is shipped' }, true) // 模拟失败 ]; console.log("--- 启动 Promise.allSettled 批量更新示例 ---"); Promise.allSettled(updates) .then(results => { console.log("所有更新请求已完成,结果如下:"); results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(` 请求 ${index + 1} 成功:`, result.value); } else { console.error(` 请求 ${index + 1} 失败:`, result.reason.message); } }); // 进一步处理:分离成功和失败的请求 const successfulUpdates = results.filter(r => r.status === 'fulfilled').map(r => r.value); const failedUpdates = results.filter(r => r.status === 'rejected').map(r => r.reason.message); console.log("n--- 分类处理结果 ---"); console.log("成功更新的服务:", successfulUpdates.map(u => u.service)); console.log("失败更新的原因:", failedUpdates); }) .finally(() => { console.log("--- Promise.allSettled 批量更新示例结束 ---"); }); -
优雅地处理部分失败
当你的应用由多个独立的部分组成,它们各自依赖不同的异步操作时,Promise.allSettled可以帮助你即使在部分操作失败的情况下,也能保持应用的核心功能运行,并向用户提供有意义的信息。场景示例:一个电子商务网站的商品详情页,需要加载商品基本信息、用户评论、相关推荐商品和库存信息。如果评论服务暂时不可用,你仍然希望用户能看到商品详情、推荐和库存,而不是整个页面空白。
代码示例:商品详情页组件加载
function loadComponentData(componentName, success, delay) { return new Promise((resolve, reject) => { setTimeout(() => { if (success) { console.log(`${componentName} 数据加载成功。`); resolve({ component: componentName, data: `这是 ${componentName} 的数据` }); } else { console.error(`${componentName} 数据加载失败!`); reject(new Error(`${componentName} 加载错误`)); } }, delay); }); } const componentPromises = [ loadComponentData('商品基本信息', true, 800), loadComponentData('用户评论', false, 1500), // 模拟评论服务失败 loadComponentData('相关推荐', true, 1200), loadComponentData('库存信息', true, 600) ]; console.log("--- 启动 Promise.allSettled 组件加载示例 ---"); Promise.allSettled(componentPromises) .then(results => { const loadedComponents = []; const failedComponents = []; results.forEach(result => { if (result.status === 'fulfilled') { loadedComponents.push(result.value); } else { failedComponents.push({ component: result.reason.message.split(' ')[0], error: result.reason.message }); } }); console.log("n--- 页面渲染结果 ---"); if (loadedComponents.length > 0) { console.log("以下组件已成功加载并显示:"); loadedComponents.forEach(comp => console.log(` - ${comp.component}: ${comp.data}`)); } else { console.log("没有组件成功加载。"); } if (failedComponents.length > 0) { console.warn("以下组件加载失败,可能需要显示占位符或错误消息:"); failedComponents.forEach(comp => console.warn(` - ${comp.component}: ${comp.error}`)); } // 更新UI逻辑... }) .finally(() => { console.log("--- Promise.allSettled 组件加载示例结束 ---"); }); -
资源清理与状态更新
在执行一系列异步操作后,你可能需要根据所有操作的最终状态来更新UI、释放资源或执行其他善后工作。由于Promise.allSettled总是解析,它非常适合作为这类操作的“终点”。场景示例:一个文件上传工具,用户可以同时上传多个文件。无论文件上传成功或失败,你都需要在所有上传完成后关闭上传进度条,并显示每个文件的上传状态。
代码示例:多文件上传状态管理
function uploadFile(fileName, simulateError = false) { return new Promise((resolve, reject) => { const delay = Math.random() * 2000 + 500; console.log(`开始上传文件: ${fileName}`); setTimeout(() => { if (simulateError && Math.random() > 0.5) { // 随机模拟失败 console.error(`文件 ${fileName} 上传失败。`); reject(new Error(`上传 ${fileName} 失败`)); } else { console.log(`文件 ${fileName} 上传成功。`); resolve({ fileName: fileName, url: `https://example.com/uploads/${fileName}` }); } }, delay); }); } const filesToUpload = ['report.pdf', 'image.jpg', 'document.docx', 'video.mp4']; const uploadPromises = filesToUpload.map(file => uploadFile(file, true)); // 随机模拟一些文件失败 console.log("--- 启动 Promise.allSettled 文件上传示例 ---"); // 假设这里有一个全局的上传进度条或状态指示器 let uploadInProgress = true; console.log("全局上传状态: 进行中..."); Promise.allSettled(uploadPromises) .then(results => { console.log("n所有文件上传任务已完成。"); results.forEach((result, index) => { const fileName = filesToUpload[index]; if (result.status === 'fulfilled') { console.log(` ${fileName}: 成功上传 (${result.value.url})`); } else { console.error(` ${fileName}: 上传失败 (${result.reason.message})`); } }); }) .catch(error => { // 这个catch块永远不会被触发,因为allSettled永远不会拒绝 console.error("意料之外的错误:", error); }) .finally(() => { // 无论成功或失败,当所有上传都settled时,我们都可以执行清理工作 uploadInProgress = false; console.log("全局上传状态: 已完成。可以关闭进度条或清理UI。"); console.log("--- Promise.allSettled 文件上传示例结束 ---"); });
边缘情况和高级模式与 Promise.allSettled
-
混合同步/异步任务
Promise.allSettled不仅能处理Promise,也能处理非Promise值。非Promise值会被立即视为已成功的Promise处理。代码示例:混合输入
const mixedTasks = [ Promise.resolve(100), 'Hello Sync', // 非 Promise 值 simulateTask('Dynamic', true, 300), Promise.reject(new Error('Sync Error')), { key: 'value' } // 非 Promise 值 ]; console.log("--- 启动 Promise.allSettled 混合任务示例 ---"); Promise.allSettled(mixedTasks) .then(results => { results.forEach(result => { console.log(result); }); }) .finally(() => { console.log("--- Promise.allSettled 混合任务示例结束 ---"); }); // 预期输出将包含 fulfilled 的 `value: 100`, `value: 'Hello Sync'`, // `value: { component: 'Dynamic', data: '这是 Dynamic 的数据' }`, // `rejected` 的 `reason: Error: Sync Error`, // `value: { key: 'value' }` -
空输入数组
如果Promise.allSettled接收一个空数组,它会立即解析为一个空数组。代码示例:空数组输入
console.log("--- 启动 Promise.allSettled 空数组示例 ---"); Promise.allSettled([]) .then(results => { console.log("空数组输入的结果:", results); // 输出: [] }) .finally(() => { console.log("--- Promise.allSettled 空数组示例结束 ---"); }); -
长时间运行的任务与超时
虽然Promise.allSettled会等待所有Promise完成,但单个Promise可能会无限期地挂起。为了防止这种情况,我们可以为每个Promise添加超时逻辑。代码示例:结合超时机制
function withTimeout(promise, timeoutMs, taskName = '未知任务') { let timeoutId; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(`${taskName} 超时 (${timeoutMs}ms)`)); }, timeoutMs); }); // Promise.race 会在原始 promise 成功或超时 promise 拒绝时率先结算 return Promise.race([promise, timeoutPromise]) .finally(() => clearTimeout(timeoutId)); // 无论哪个先完成,都清除定时器 } const longRunningTask = simulateTask('LongTask', true, 5000); // 5秒后成功 const quickTask = simulateTask('QuickTask', true, 800); const failingTask = simulateTask('FailingTask', false, 1000); const tasksWithTimeout = [ withTimeout(longRunningTask, 2000, 'LongTask'), // LongTask 会超时 withTimeout(quickTask, 2000, 'QuickTask'), withTimeout(failingTask, 5000, 'FailingTask') // FailingTask 在超时前失败 ]; console.log("--- 启动 Promise.allSettled 结合超时示例 ---"); Promise.allSettled(tasksWithTimeout) .then(results => { console.log("所有任务(含超时处理)已完成:"); results.forEach(result => { if (result.status === 'fulfilled') { console.log(` 成功: ${result.value.name || result.value}`); } else { console.error(` 失败: ${result.reason.message}`); } }); }) .finally(() => { console.log("--- Promise.allSettled 结合超时示例结束 ---"); }); -
在结果数组中筛选和处理
Promise.allSettled返回的结构化结果非常适合进行后续的筛选、映射和统计。代码示例:筛选成功和失败的任务
const tasks = [ simulateTask('DataFetch1', true, 700), simulateTask('DataFetch2', false, 300), simulateTask('DataFetch3', true, 900), simulateTask('DataFetch4', false, 500) ]; console.log("--- 启动 Promise.allSettled 结果筛选示例 ---"); Promise.allSettled(tasks) .then(results => { const successfulResults = results .filter(result => result.status === 'fulfilled') .map(result => result.value); const failedReasons = results .filter(result => result.status === 'rejected') .map(result => result.reason.message); console.log("成功获取的数据:", successfulResults); console.log("失败原因列表:", failedReasons); // 统计成功率 const successCount = successfulResults.length; const totalCount = results.length; console.log(`任务成功率: ${((successCount / totalCount) * 100).toFixed(2)}%`); }) .finally(() => { console.log("--- Promise.allSettled 结果筛选示例结束 ---"); }); -
与重试机制结合
当某个任务可能因为瞬时网络问题而失败时,重试是常见的策略。Promise.allSettled可以帮助我们收集所有(包括重试后的)任务的最终状态。代码示例:结合简单重试
async function retry(fn, retries = 3, delay = 100) { for (let i = 0; i < retries; i++) { try { return await fn(); } catch (error) { console.warn(`重试 ${fn.name || '任务'} (第 ${i + 1} 次) 失败: ${error.message}`); if (i < retries - 1) { await new Promise(resolve => setTimeout(resolve, delay * (i + 1))); // 递增延迟 } else { throw error; // 最后一次尝试仍失败,则抛出错误 } } } } // 模拟一个有时会失败的任务 let attemptCount = 0; function flakyTask(id) { return new Promise((resolve, reject) => { attemptCount++; const shouldFail = Math.random() < 0.7 && attemptCount <= 2; // 前两次尝试有较高几率失败 setTimeout(() => { if (shouldFail) { console.log(`任务 ${id} (尝试 ${attemptCount}) 失败。`); reject(new Error(`网络不稳定,任务 ${id} 失败`)); } else { console.log(`任务 ${id} (尝试 ${attemptCount}) 成功。`); resolve(`数据-${id}-成功`); } }, 300); }); } const tasksToRun = [ retry(() => flakyTask('A'), 3), retry(() => flakyTask('B'), 3), retry(() => simulateTask('C', true, 200), 1), // 正常任务不需要重试 retry(() => simulateTask('D', false, 200), 1) // 必然失败的任务 ]; console.log("--- 启动 Promise.allSettled 结合重试示例 ---"); Promise.allSettled(tasksToRun) .then(results => { console.log("n所有任务(含重试)的最终结果:"); results.forEach((result, index) => { const taskName = ['A', 'B', 'C', 'D'][index]; if (result.status === 'fulfilled') { console.log(` 任务 ${taskName} 最终成功:`, result.value); } else { console.error(` 任务 ${taskName} 最终失败:`, result.reason.message); } }); }) .finally(() => { console.log("--- Promise.allSettled 结合重试示例结束 ---"); });
Promise.any 深度解析
Promise.any 方法返回一个Promise,只要给定的可迭代对象中的任何一个Promise成功(fulfilled),这个Promise就会立即成功,并返回该Promise的成功值。如果所有给定的Promise都失败(rejected),则返回的Promise会以一个 AggregateError 拒绝,其中包含了所有失败的原因。
关键特性总结:
- “第一成功者胜出”:只要有一个Promise成功,
Promise.any就会立即解析,并忽略其他Promise的成功或失败。 - 拒绝时机:只有当所有传入的Promise都拒绝时,
Promise.any才会拒绝。 - 拒绝类型:如果拒绝,它会抛出一个
AggregateError,这是一个新的错误类型,它将所有Promise的拒绝原因存储在一个errors数组中。
核心应用场景
-
竞速获取数据
当你需要从多个可能的数据源(例如,多个CDN、镜像服务器、或不同的API版本)获取相同的数据,并且你只关心哪个源能最快响应时,Promise.any是理想选择。场景示例:用户从多个地理位置分散的CDN下载一个大文件。你希望选择最快响应的CDN,以优化用户体验。
代码示例:多CDN下载竞速
function downloadFromCDN(cdnName, delay, failRate = 0) { return new Promise((resolve, reject) => { setTimeout(() => { if (Math.random() < failRate) { console.error(`CDN ${cdnName} 下载失败!`); reject(new Error(`CDN ${cdnName} 不可用`)); } else { console.log(`CDN ${cdnName} 下载成功,耗时 ${delay}ms。`); resolve(`数据来自 ${cdnName}`); } }, delay); }); } const cdnServers = [ downloadFromCDN('CDN-A', 1500, 0.2), // 可能会失败 downloadFromCDN('CDN-B', 800, 0.1), downloadFromCDN('CDN-C', 2200, 0.3), downloadFromCDN('CDN-D', 500, 0.05) // 最快,但也有小概率失败 ]; console.log("--- 启动 Promise.any CDN 竞速示例 ---"); Promise.any(cdnServers) .then(result => { console.log("成功获取数据:", result); // 这里只会打印第一个成功的结果 }) .catch(error => { // 只有当所有 CDN 都失败时,才会进入这里 console.error("所有 CDN 下载均失败!", error.errors.map(e => e.message)); }) .finally(() => { console.log("--- Promise.any CDN 竞速示例结束 ---"); }); -
提供备用方案 (Fallback)
当你有多个获取数据或执行操作的策略,并且希望按优先级尝试它们,只要其中一个成功就足够时,Promise.any提供了一种优雅的实现方式。场景示例:尝试从主API获取用户配置,如果主API失败,则尝试从备用API获取,如果备用API也失败,则尝试从本地缓存获取。
代码示例:多源配置加载
function fetchFromMainAPI() { return new Promise((resolve, reject) => { console.log("尝试从主API获取配置..."); setTimeout(() => { // Math.random() < 0.7 模拟主API经常失败 if (Math.random() < 0.7) { reject(new Error("主API服务器错误")); } else { resolve({ source: 'MainAPI', settings: { theme: 'dark', language: 'en' } }); } }, 1000); }); } function fetchFromBackupAPI() { return new Promise((resolve, reject) => { console.log("尝试从备用API获取配置..."); setTimeout(() => { // Math.random() < 0.5 模拟备用API有时也会失败 if (Math.random() < 0.5) { reject(new Error("备用API服务器错误")); } else { resolve({ source: 'BackupAPI', settings: { theme: 'light', language: 'zh' } }); } }, 800); }); } function fetchFromLocalCache() { return new Promise((resolve, reject) => { console.log("尝试从本地缓存获取配置..."); setTimeout(() => { const cachedSettings = { theme: 'system', language: 'auto' }; if (cachedSettings) { resolve({ source: 'LocalCache', settings: cachedSettings }); } else { reject(new Error("本地缓存无可用配置")); } }, 200); }); } console.log("--- 启动 Promise.any 配置加载示例 ---"); Promise.any([ fetchFromMainAPI(), fetchFromBackupAPI(), fetchFromLocalCache() ]) .then(config => { console.log("成功加载用户配置:", config); }) .catch(error => { // 只有所有尝试都失败时才进入 console.error("所有配置加载尝试均失败!", error.errors.map(e => e.message)); }) .finally(() => { console.log("--- Promise.any 配置加载示例结束 ---"); }); -
用户体验优化
在某些场景下,为了尽快向用户展示内容,你可以使用Promise.any。例如,加载图片时,可以同时尝试加载不同分辨率或不同格式的图片,优先显示最快加载成功的那个。场景示例:加载一个用户头像。你可能有一个CDN URL,一个备用图片服务器URL,甚至一个本地缓存的Base64字符串。只要能显示一个,用户体验就比等待所有尝试都失败要好。
代码示例:图片加载优化
function loadImage(sourceType, url, delay, shouldFail = false) { return new Promise((resolve, reject) => { console.log(`尝试从 ${sourceType} 加载图片: ${url}`); setTimeout(() => { if (shouldFail) { console.error(`${sourceType} 图片加载失败: ${url}`); reject(new Error(`${sourceType} 加载失败`)); } else { console.log(`${sourceType} 图片加载成功: ${url}`); resolve({ source: sourceType, imageUrl: url, displayTime: delay }); } }, delay); }); } const imageUrls = [ loadImage('CDN', 'https://cdn.example.com/avatar.jpg', 1200, Math.random() > 0.8), loadImage('BackupServer', 'https://backup.example.com/avatar.jpg', 800, Math.random() > 0.6), loadImage('LocalCache', 'data:image/jpeg;base64,...', 100) // 模拟本地缓存,通常最快 ]; console.log("--- 启动 Promise.any 图片加载优化示例 ---"); Promise.any(imageUrls) .then(imageData => { console.log(`图片已显示!来自 ${imageData.source},URL: ${imageData.imageUrl}`); // 可以在这里更新 DOM,显示图片 // document.getElementById('avatar').src = imageData.imageUrl; }) .catch(error => { console.error("所有图片加载尝试均失败!", error.errors.map(e => e.message)); // 显示默认头像或错误占位符 }) .finally(() => { console.log("--- Promise.any 图片加载优化示例结束 ---"); });
边缘情况和高级模式与 Promise.any
-
所有任务都失败的情况
这是Promise.any独特的边缘情况。当所有Promise都拒绝时,Promise.any会以AggregateError拒绝。你需要特别处理这个错误类型来获取所有失败的原因。代码示例:所有任务均失败
const allFailingTasks = [ simulateTask('FailingA', false, 500), simulateTask('FailingB', false, 1000), simulateTask('FailingC', false, 200) ]; console.log("--- 启动 Promise.any 所有任务失败示例 ---"); Promise.any(allFailingTasks) .then(result => { console.log("意外的成功:", result); // 不会执行 }) .catch(error => { console.error("所有任务都失败了!错误详情:"); if (error instanceof AggregateError) { error.errors.forEach((err, index) => { console.error(` 任务 ${index + 1} 拒绝原因: ${err.message}`); }); } else { console.error("非 AggregateError 错误:", error.message); } }) .finally(() => { console.log("--- Promise.any 所有任务失败示例结束 ---"); }); -
空输入数组
如果Promise.any接收一个空数组,它会立即拒绝,并带有一个AggregateError,其errors数组为空。代码示例:空数组输入
console.log("--- 启动 Promise.any 空数组示例 ---"); Promise.any([]) .then(result => { console.log("意外的成功:", result); // 不会执行 }) .catch(error => { console.error("空数组输入导致拒绝!错误详情:"); if (error instanceof AggregateError) { console.error(" AggregateError 捕获到。errors 数组:", error.errors); // [] } else { console.error("非 AggregateError 错误:", error.message); } }) .finally(() => { console.log("--- Promise.any 空数组示例结束 ---"); }); -
混合同步/异步任务
与Promise.allSettled类似,Promise.any也会将非Promise值视为已解析的Promise。如果数组中包含非Promise的同步值,并且该值不是拒绝,那么Promise.any会立即解析为该值。代码示例:混合输入
const mixedAnyTasks = [ simulateTask('SlowReject', false, 1000), 'Instant Success!', // 非 Promise 值,会被立即解析 Promise.resolve('Another Success'), simulateTask('QuickReject', false, 100) ]; console.log("--- 启动 Promise.any 混合任务示例 ---"); Promise.any(mixedAnyTasks) .then(result => { console.log("第一个成功结果:", result); // 预期是 'Instant Success!' }) .catch(error => { console.error("所有任务失败:", error.errors.map(e => e.message)); }) .finally(() => { console.log("--- Promise.any 混合任务示例结束 ---"); }); -
与超时机制结合
虽然Promise.any自身就具备“竞速”的特性,但如果你希望整个Promise.any操作本身也有一个总的超时时间,可以在外部再嵌套一个Promise.race。代码示例:Promise.any 的总超时
function createTimeoutPromise(ms, message) { return new Promise((_, reject) => { setTimeout(() => reject(new Error(message)), ms); }); } const tasksToRace = [ simulateTask('Task1', true, 1500), simulateTask('Task2', false, 800), simulateTask('Task3', true, 2000) ]; const overallTimeout = createTimeoutPromise(1000, 'Promise.any 操作整体超时'); // 1秒超时 console.log("--- 启动 Promise.any 整体超时示例 ---"); Promise.race([ Promise.any(tasksToRace), overallTimeout ]) .then(result => { console.log("Promise.any 成功,在总超时前:", result); }) .catch(error => { console.error("Promise.any 失败或总超时:", error.message || error.errors.map(e => e.message)); }) .finally(() => { console.log("--- Promise.any 整体超时示例结束 ---"); });在这个例子中,如果
Task1或Task3在1秒内成功,或者Task2拒绝但有其他任务成功,Promise.any会解析。但如果所有任务都在1秒内拒绝,或者所有任务都慢于1秒,那么overallTimeout将会先拒绝,导致Promise.race拒绝。 -
需要至少 N 个成功
Promise.any仅适用于“至少一个成功”。如果你需要“至少N个成功”,例如,需要从5个服务器中至少有3个成功响应,那么Promise.any就不适用了。这种场景下,你通常会结合Promise.allSettled和后续的筛选逻辑来实现。讨论:至少 N 个成功的需求
对于“至少N个成功”的需求,我们不能直接使用Promise.any。一个常见的模式是:- 使用
Promise.allSettled获取所有任务的结果。 - 过滤出所有成功的任务。
- 检查成功任务的数量是否达到
N。 - 根据检查结果决定后续操作。
伪代码示例:至少 N 个成功
async function atLeastNSuccess(promises, n) { const results = await Promise.allSettled(promises); const successfulResults = results.filter(p => p.status === 'fulfilled'); if (successfulResults.length >= n) { console.log(`成功找到至少 ${n} 个任务!`); return successfulResults.map(p => p.value); } else { const failedReasons = results.filter(p => p.status === 'rejected').map(p => p.reason.message); throw new Error(`未能达到至少 ${n} 个成功。仅 ${successfulResults.length} 个成功。失败原因: ${failedReasons.join('; ')}`); } } // 示例使用 // atLeastNSuccess([task1, task2, task3, task4, task5], 3) // .then(successfulData => console.log(successfulData)) // .catch(error => console.error(error)); - 使用
Promise.allSettled 与 Promise.any 的对比与选择
现在我们已经深入了解了 Promise.allSettled 和 Promise.any,让我们通过一个表格来总结它们的关键差异,并提供一个决策指南。
| 特性 | Promise.all |
Promise.allSettled |
Promise.any |
Promise.race |
|---|---|---|---|---|
| 解决时机 | 所有Promise都成功 | 所有Promise都settled |
任何一个Promise成功 | 任何一个Promisesettled |
| 拒绝时机 | 任何一个Promise拒绝 | 永不拒绝 | 所有Promise都拒绝 | 任何一个Promise拒绝 |
| 返回类型 | Promise<Array<Value>> |
Promise<Array<ResultObject>> |
Promise<Value> |
Promise<Value | Reason> |
| 拒绝原因 | 第一个拒绝的Promise的reason |
N/A (永不拒绝) | AggregateError (包含所有reason) |
第一个settled的Promise的reason |
| 结果信息 | 仅成功结果,失败时无 | 所有Promise的status, value/reason |
仅第一个成功结果,失败时所有reason |
仅第一个settled的结果/原因 |
| 典型应用场景 | 必须所有任务都成功才能继续的批量操作 | 收集所有任务结果,无论成功与否;优雅处理部分失败 | 竞速获取数据;提供备用方案;用户体验优化 (取最快) | 任务超时;取最快完成的任务(成功或失败) |
| “Fail-fast” | 是 | 否 | 否 | 是 (如果第一个是失败) |
何时选择哪个 Promise 组合器?
-
使用
Promise.all当:- 你需要执行一组异步操作,并且这些操作是“原子性”的,即它们必须全部成功才能进行下一步。
- 任何一个操作的失败都意味着整个批处理的失败,你希望立即知道这个失败。
- 你只关心所有操作都成功时的结果。
-
使用
Promise.allSettled当:- 你需要执行一组独立的异步操作,并且你关心每个操作的最终结果,无论它们成功还是失败。
- 即使部分操作失败,你也不希望整个流程中断,而是希望能够基于已完成的操作进行后续处理。
- 你需要为用户提供详细的反馈,说明哪些任务成功,哪些任务失败,以及失败的原因。
-
使用
Promise.any当:- 你有多个可选的异步操作来完成同一个目标,并且你只关心其中任何一个操作能够成功。
- 你希望尽快获取一个成功的结果,而不必等待所有操作完成。
- 你希望实现“竞速”或“备用方案”的逻辑,以提高响应速度或鲁棒性。
-
使用
Promise.race当:- 你只关心最快完成(无论成功或失败)的那个Promise的结果。
- 你希望为一组Promise设置一个总的超时时间。
一个复杂场景的整合示例:用户数据仪表盘
假设我们正在构建一个用户数据仪表盘,它需要从多个服务获取数据。
- 用户基本信息:非常关键,需要从多个数据源尝试获取,只要一个成功即可显示。
- 近期活动列表:从多个日志服务获取,有些可能失败,但我们仍希望显示所有能成功获取的活动。
- 系统健康检查:多个关键服务必须全部正常,否则显示系统异常。
// 模拟各种服务
function fetchUserInfo(source, delay, fail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (fail && Math.random() > 0.3) { // 模拟随机失败
console.warn(`[用户服务] ${source} 获取失败.`);
reject(new Error(`${source} 用户信息获取失败`));
} else {
console.log(`[用户服务] ${source} 获取成功.`);
resolve({ source: source, user: { id: 'U123', name: 'Dashboard User', email: `${source}@example.com` } });
}
}, delay);
});
}
function fetchRecentActivity(service, delay, fail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (fail && Math.random() > 0.4) {
console.warn(`[活动服务] ${service} 获取失败.`);
reject(new Error(`${service} 活动数据获取失败`));
} else {
console.log(`[活动服务] ${service} 获取成功.`);
resolve({ service: service, activities: [`${service} - activity A`, `${service} - activity B`] });
}
}, delay);
});
}
function checkSystemService(name, delay, fail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (fail && Math.random() > 0.2) {
console.error(`[健康检查] ${name} 服务异常!`);
reject(new Error(`${name} 服务故障`));
} else {
console.log(`[健康检查] ${name} 服务正常.`);
resolve({ service: name, status: 'OK' });
}
}, delay);
});
}
async function loadDashboard() {
console.log("--- 启动仪表盘加载 ---");
// 1. 用户基本信息 (使用 Promise.any - 取最快成功的)
console.log("n--- 加载用户基本信息 (Promise.any) ---");
const userInfoPromises = [
fetchUserInfo('PrimaryAPI', 1500, true), // 主API可能失败
fetchUserInfo('SecondaryAPI', 800, true), // 备用API也可能失败
fetchUserInfo('Cache', 200, false) // 本地缓存最快且稳定
];
let userInfo = null;
try {
userInfo = await Promise.any(userInfoPromises);
console.log("用户基本信息已加载:", userInfo.user.name);
} catch (error) {
console.error("未能加载用户基本信息!", error.errors.map(e => e.message));
userInfo = { user: { id: 'N/A', name: '访客', email: '[email protected]' } }; // 提供默认值
}
// 2. 近期活动列表 (使用 Promise.allSettled - 收集所有结果)
console.log("n--- 加载近期活动列表 (Promise.allSettled) ---");
const activityPromises = [
fetchRecentActivity('LogServiceA', 1000, true), // 可能失败
fetchRecentActivity('LogServiceB', 1200, false),
fetchRecentActivity('LogServiceC', 700, true) // 可能失败
];
const activityResults = await Promise.allSettled(activityPromises);
const successfulActivities = [];
const failedActivityServices = [];
activityResults.forEach(result => {
if (result.status === 'fulfilled') {
successfulActivities.push(...result.value.activities);
} else {
failedActivityServices.push(result.reason.message);
}
});
console.log("已加载活动:", successfulActivities);
if (failedActivityServices.length > 0) {
console.warn("部分活动服务加载失败:", failedActivityServices);
}
// 3. 系统健康检查 (使用 Promise.all - 必须全部成功)
console.log("n--- 进行系统健康检查 (Promise.all) ---");
const healthCheckPromises = [
checkSystemService('AuthService', 500, false),
checkSystemService('DbService', 800, false),
checkSystemService('MessageQueue', 300, true) // 模拟消息队列服务可能异常
];
let systemHealth = '正常';
try {
await Promise.all(healthCheckPromises);
console.log("所有核心服务运行正常。");
} catch (error) {
systemHealth = `异常: ${error.message}`;
console.error("核心系统服务故障!", error.message);
}
console.log("n--- 仪表盘最终状态 ---");
console.log(`当前用户: ${userInfo.user.name}`);
console.log(`近期活动 (${successfulActivities.length} 条):`, successfulActivities.slice(0, 5));
console.log(`系统健康状态: ${systemHealth}`);
console.log("--- 仪表盘加载完成 ---");
}
loadDashboard();
通过这个综合示例,我们清晰地看到了 Promise.any、Promise.allSettled 和 Promise.all 如何在同一个应用中协同工作,以满足不同组件对异步操作的不同需求。
结语
Promise.allSettled 和 Promise.any 是 JavaScript 异步编程工具箱中不可或缺的利器。它们极大地增强了我们处理并发Promise的能力,特别是在面对复杂的异步流程和边缘情况时。理解并恰当地运用它们,能够帮助我们构建更加健壮、容错性更强、用户体验更佳的应用程序。在选择Promise组合器时,请始终思考你的核心需求:是需要所有结果,是只要一个成功,还是所有都必须成功?选择正确的工具,你的代码将更加优雅和强大。