Promise.allSettled 与 Promise.any 的应用场景:处理多个异步任务的边缘情况

各位编程专家、异步编程爱好者们,大家好!

在现代Web开发中,异步操作无处不在。从网络请求到文件读写,再到定时任务,我们几乎时刻都在与Promise打交道。Promise.all无疑是我们最常用的Promise组合器之一,它能并行执行多个Promise,并在所有Promise都成功时返回一个结果数组,或在任何一个Promise失败时立即拒绝。然而,这种“全有或全无”的模式,在许多复杂的真实世界场景中,显得过于严格。

设想一下,你正在构建一个仪表盘应用,它需要从多个不同的服务获取数据来填充不同的组件。如果其中一个服务暂时不可用,你是否希望整个仪表盘都崩溃,还是希望那些能成功加载的组件依然能正常显示?再比如,你需要从多个镜像服务器下载同一个资源,你只关心哪个服务器能最快地响应并提供数据。如果所有镜像都尝试失败了,你才需要知道这个情况。

这些场景正是 Promise.allSettledPromise.any 这两个强大的Promise组合器大放异彩的地方。它们是ES2020引入的,旨在弥补 Promise.allPromise.race 在特定边缘情况下的不足,为我们处理并发异步任务提供了更精细、更灵活的控制。

今天,我们将深入探讨 Promise.allSettledPromise.any 的工作原理、核心特性,以及它们在各种应用场景,特别是那些棘手的边缘情况下的应用。我们将通过丰富的代码示例,理解如何利用它们构建更加健壮、用户体验更佳的异步系统。

Promise 基础回顾与 Promise.all 的局限性

在深入 Promise.allSettledPromise.any 之前,我们先快速回顾一下Promise的基本状态和 Promise.all 的行为。

一个Promise有三种状态:

  1. pending (待定):初始状态,既没有成功,也没有失败。
  2. fulfilled (已成功):操作成功完成。
  3. rejected (已失败):操作失败。

Promise的状态一旦从 pending 变为 fulfilledrejected,就称为 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 块。

从上面的例子可以看出,尽管 taskAtaskB 最终会成功,但 taskC 的快速失败导致整个 Promise.all 链条中断,我们无法得知 taskAtaskB 的执行情况,也无法获取它们的结果。

这种“fail-fast”行为在某些场景下是合理的,例如:

  • 强一致性要求:所有数据必须全部获取成功才能进行下一步操作。
  • 原子性操作:一组操作必须全部成功才能视为完成,否则全部回滚。

然而,在更多场景下,我们可能需要:

  1. 无论成功或失败,都想知道所有任务的最终状态和结果。
  2. 只要有任何一个任务成功,就立即处理,而不需要等待所有任务。
  3. 即使部分任务失败,也希望继续处理那些成功的任务。

为了解决这些需求,ES2020引入了 Promise.allSettledPromise.any

Promise.allSettled 深度解析

Promise.allSettled 方法返回一个在所有给定的Promise都已经settled(即fulfilledrejected)后解析的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提供详细的 statusvaluereason

核心应用场景

  1. 收集所有任务结果,无论成功或失败
    这是 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 批量更新示例结束 ---");
        });
  2. 优雅地处理部分失败
    当你的应用由多个独立的部分组成,它们各自依赖不同的异步操作时,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 组件加载示例结束 ---");
        });
  3. 资源清理与状态更新
    在执行一系列异步操作后,你可能需要根据所有操作的最终状态来更新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

  1. 混合同步/异步任务
    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' }`
  2. 空输入数组
    如果 Promise.allSettled 接收一个空数组,它会立即解析为一个空数组。

    代码示例:空数组输入

    console.log("--- 启动 Promise.allSettled 空数组示例 ---");
    Promise.allSettled([])
        .then(results => {
            console.log("空数组输入的结果:", results); // 输出: []
        })
        .finally(() => {
            console.log("--- Promise.allSettled 空数组示例结束 ---");
        });
  3. 长时间运行的任务与超时
    虽然 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 结合超时示例结束 ---");
        });
  4. 在结果数组中筛选和处理
    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 结果筛选示例结束 ---");
        });
  5. 与重试机制结合
    当某个任务可能因为瞬时网络问题而失败时,重试是常见的策略。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 数组中。

核心应用场景

  1. 竞速获取数据
    当你需要从多个可能的数据源(例如,多个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 竞速示例结束 ---");
        });
  2. 提供备用方案 (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 配置加载示例结束 ---");
        });
  3. 用户体验优化
    在某些场景下,为了尽快向用户展示内容,你可以使用 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

  1. 所有任务都失败的情况
    这是 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 所有任务失败示例结束 ---");
        });
  2. 空输入数组
    如果 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 空数组示例结束 ---");
        });
  3. 混合同步/异步任务
    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 混合任务示例结束 ---");
        });
  4. 与超时机制结合
    虽然 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 整体超时示例结束 ---");
        });

    在这个例子中,如果 Task1Task3 在1秒内成功,或者 Task2 拒绝但有其他任务成功,Promise.any 会解析。但如果所有任务都在1秒内拒绝,或者所有任务都慢于1秒,那么 overallTimeout 将会先拒绝,导致 Promise.race 拒绝。

  5. 需要至少 N 个成功
    Promise.any 仅适用于“至少一个成功”。如果你需要“至少N个成功”,例如,需要从5个服务器中至少有3个成功响应,那么 Promise.any 就不适用了。这种场景下,你通常会结合 Promise.allSettled 和后续的筛选逻辑来实现。

    讨论:至少 N 个成功的需求
    对于“至少N个成功”的需求,我们不能直接使用 Promise.any。一个常见的模式是:

    1. 使用 Promise.allSettled 获取所有任务的结果。
    2. 过滤出所有成功的任务。
    3. 检查成功任务的数量是否达到 N
    4. 根据检查结果决定后续操作。

    伪代码示例:至少 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.allSettledPromise.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.anyPromise.allSettledPromise.all 如何在同一个应用中协同工作,以满足不同组件对异步操作的不同需求。

结语

Promise.allSettledPromise.any 是 JavaScript 异步编程工具箱中不可或缺的利器。它们极大地增强了我们处理并发Promise的能力,特别是在面对复杂的异步流程和边缘情况时。理解并恰当地运用它们,能够帮助我们构建更加健壮、容错性更强、用户体验更佳的应用程序。在选择Promise组合器时,请始终思考你的核心需求:是需要所有结果,是只要一个成功,还是所有都必须成功?选择正确的工具,你的代码将更加优雅和强大。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注