Promise.allSettled 与 Promise.any 的底层实现:基于异步序列与状态追踪的算法分析

各位同仁,各位技术爱好者,大家好。

今天,我们将深入探讨JavaScript异步编程领域中两个功能强大且至关重要的Promise组合器:Promise.allSettledPromise.any。它们为我们处理并发异步操作提供了精细的控制,但其底层实现机制,即如何基于异步序列与状态追踪来协同工作,才是我们本次讲座的核心。我们将从零开始,剖析它们的算法原理、实现细节,并辅以详尽的代码示例,力求揭示其内部运作的奥秘。

异步编程的基石:Promise与微任务队列

在深入探讨Promise.allSettledPromise.any之前,我们必须先回顾Promise的基础及其在JavaScript运行时环境中的作用。Promise是处理异步操作结果的对象,它有三种状态:

  1. Pending (待定):初始状态,既不是成功也不是失败。
  2. Fulfilled (已成功):操作成功完成。
  3. Rejected (已失败):操作失败。

Promise的状态一旦从Pending变为Fulfilled或Rejected,就不可逆转。这个过程是异步的,其回调(通过.then().catch().finally()注册)被调度到微任务队列 (Microtask Queue)

JavaScript的事件循环 (Event Loop) 是其并发模型的核心。它不断地检查调用栈是否为空,然后处理任务队列(宏任务,如setTimeoutsetInterval、I/O操作)中的任务,之后在每次事件循环迭代结束时,会清空微任务队列。这意味着Promise的回调具有高优先级,会在当前宏任务执行完毕后、下一个宏任务开始前执行。这种机制是所有Promise组合器能够有效运作的基石。

Promise.allSettled 的底层实现:全面洞察与结果聚合

Promise.allSettled是一个相对较新的Promise组合器,它解决了Promise.all在处理部分失败场景时的痛点。Promise.all在任何一个Promise拒绝时就会立即拒绝,而我们有时需要知道所有异步操作的最终状态,无论它们成功还是失败。Promise.allSettled正是为此而生。

1. Promise.allSettled 的核心目的与使用场景

当我们需要并行执行多个异步操作,并且希望在所有操作都完成(无论是成功还是失败)后,获取每个操作的最终结果及状态时,Promise.allSettled是理想的选择。

使用场景示例:

  • 批量发送邮件或通知,需要记录每封邮件的发送结果(成功或失败原因)。
  • 并行请求多个API,即使部分API失败,也希望汇总所有API的响应,以便用户了解哪些数据已加载,哪些未能加载。
  • 在用户界面中显示多个组件的加载状态,每个组件的加载可能成功也可能失败。

2. 算法分析:异步序列与状态追踪

Promise.allSettled的实现依赖于以下几个关键算法步骤:

  1. 创建返回Promise: Promise.allSettled函数本身会立即返回一个新的Promise,我们称之为“主Promise”。这个主Promise的命运(解析或拒绝)将取决于所有输入Promise的最终状态。
  2. 输入规范化与空数组处理:
    • 首先,它会将输入的可迭代对象(通常是数组)中的每个元素通过 Promise.resolve() 转换为一个真正的Promise。这确保了即使输入数组中包含非Promise值,也能被正确处理。
    • 如果输入数组为空,主Promise会立即以一个空数组进行解析。
  3. 状态追踪机制:
    • 初始化一个结果数组,其长度与输入Promise数组相同。每个位置将存储对应Promise的最终状态对象。
    • 初始化一个计数器,用于跟踪已结算 (settled) 的Promise数量。一个Promise被认为是“结算”的,当它变为Fulfilled或Rejected状态时。
  4. 并行监听与结果记录:
    • 遍历输入Promise数组。对于每一个Promise p,我们都为其添加.then().catch()处理器(或者更简洁地使用.finally()来统一处理,但内部仍需区分成功与失败)。
    • 核心逻辑:
      • 无论p是成功(Fulfilled)还是失败(Rejected),都会在对应的回调中记录其状态。
      • 如果p成功,记录 { status: 'fulfilled', value: <成功值> } 到结果数组的对应位置。
      • 如果p失败,记录 { status: 'rejected', reason: <失败原因> } 到结果数组的对应位置。
      • 每当一个Promise结算,计数器加一。
  5. 主Promise的解析:
    • 在每个Promise的结算回调中,都会检查已结算的Promise数量是否等于输入Promise的总数。
    • 一旦所有输入Promise都已结算,主Promise就会以完整的、包含所有状态对象的结果数组进行解析。

重要的是,Promise.allSettled不会因为任何一个Promise的拒绝而提前拒绝。它会耐心等待所有Promise都完成各自的使命。

3. 详细算法步骤与伪代码

为了更清晰地理解,我们可以将上述算法细化为以下步骤:

  1. 函数签名: myAllSettled(promises)
  2. 参数检查: 确保promises是一个可迭代对象。
  3. 初始化:
    • results = new Array(promises.length)
    • settledCount = 0
    • totalPromises = promises.length
  4. 空数组处理:
    • 如果 totalPromises === 0,则立即返回 Promise.resolve([])
  5. 创建并返回主Promise:
    • return new Promise((resolve, reject) => { ... })
  6. 迭代与监听:
    • 对于 promises 数组中的每个 promise (及其索引 index):
      • Promise.resolve(promise) 将其标准化为Promise p
      • p.then(value => { ... })
        • results[index] = { status: 'fulfilled', value: value }
        • settledCount++
        • checkCompletion()
      • p.catch(reason => { ... })
        • results[index] = { status: 'rejected', reason: reason }
        • settledCount++
        • checkCompletion()
  7. 完成检查函数 checkCompletion()
    • 如果 settledCount === totalPromises,则 resolve(results)

4. 代码实现 (myAllSettled)

/**
 * 模拟实现 Promise.allSettled
 * @param {Array<Promise<any> | any>} promises - 一个Promise或值的可迭代对象
 * @returns {Promise<Array<{status: 'fulfilled', value: any} | {status: 'rejected', reason: any}>>}
 */
function myAllSettled(promises) {
    // 1. 确保输入是一个可迭代对象,并将其转换为数组
    const promiseArray = Array.isArray(promises) ? promises : [...promises];
    const totalPromises = promiseArray.length;

    // 2. 如果输入数组为空,立即返回一个已解析为[]的Promise
    if (totalPromises === 0) {
        return Promise.resolve([]);
    }

    // 3. 创建一个数组来存储每个Promise的结果
    const results = new Array(totalPromises);
    // 4. 初始化一个计数器,用于跟踪已结算的Promise数量
    let settledCount = 0;

    // 5. 返回一个新的Promise,其解析结果将是所有Promise的结算状态数组
    return new Promise((resolve) => {
        // 遍历每个输入的Promise或值
        promiseArray.forEach((p, index) => {
            // 6. 将当前项标准化为Promise,以处理非Promise值
            Promise.resolve(p)
                .then(value => {
                    // 7. 记录成功状态
                    results[index] = { status: 'fulfilled', value: value };
                })
                .catch(reason => {
                    // 8. 记录失败状态
                    results[index] = { status: 'rejected', reason: reason };
                })
                .finally(() => {
                    // 9. 无论成功或失败,都增加已结算的计数器
                    settledCount++;
                    // 10. 检查是否所有Promise都已结算
                    if (settledCount === totalPromises) {
                        // 11. 如果所有Promise都已结算,解析主Promise,返回结果数组
                        resolve(results);
                    }
                });
        });
    });
}

// 示例用法
const p1 = Promise.resolve(1);
const p2 = new Promise((resolve) => setTimeout(() => resolve(2), 50));
const p3 = Promise.reject('Error occurred');
const p4 = 4; // 非Promise值

myAllSettled([p1, p2, p3, p4])
    .then(results => {
        console.log("myAllSettled Results:");
        console.log(results);
        /* 预期输出 (顺序可能因异步完成时间略有不同,但最终一致):
        [
          { status: 'fulfilled', value: 1 },
          { status: 'fulfilled', value: 2 },
          { status: 'rejected', reason: 'Error occurred' },
          { status: 'fulfilled', value: 4 }
        ]
        */
    });

myAllSettled([])
    .then(results => {
        console.log("myAllSettled with empty array:", results); // []
    });

myAllSettled([Promise.reject('a'), Promise.reject('b')])
    .then(results => {
        console.log("myAllSettled with all rejections:", results);
        /*
        [
          { status: 'rejected', reason: 'a' },
          { status: 'rejected', reason: 'b' }
        ]
        */
    });

5. 状态追踪的表格表示

为了更直观地理解myAllSettled如何追踪状态,我们可以构建一个简化的追踪表。假设我们有三个Promise:P_A (resolve ‘A’),P_B (reject ‘B’),P_C (resolve ‘C’)。

事件发生 P_A 状态 P_B 状态 P_C 状态 results 数组 (简化) settledCount 主Promise状态
初始化 Pending Pending Pending [undefined, undefined, undefined] 0 Pending
P_A 解决 Fulfilled Pending Pending [{s:'f',v:'A'}, undefined, undefined] 1 Pending
P_C 解决 Fulfilled Pending Fulfilled [{s:'f',v:'A'}, undefined, {s:'f',v:'C'}] 2 Pending
P_B 拒绝 Fulfilled Rejected Fulfilled [{s:'f',v:'A'}, {s:'r',r:'B'}, {s:'f',v:'C'}] 3 Pending
检查完成 Fulfilled Rejected Fulfilled [{s:'f',v:'A'}, {s:'r',r:'B'}, {s:'f',v:'C'}] 3 (== total) Resolved

从表中可以看出,settledCount是关键的协调变量,它确保了只有在所有Promise都完成其生命周期后,主Promise才会被解析。

Promise.any 的底层实现:追求速度与短路机制

Promise.any是另一个强大的Promise组合器,它与Promise.allPromise.race都有所不同。它的核心思想是“只要有一个成功,我就成功”。如果所有Promise都失败,那么它才会失败。

1. Promise.any 的核心目的与使用场景

当我们需要并行执行多个异步操作,并且只要其中任何一个操作成功,我们就接受其结果,而忽略其他操作的成功或失败时,Promise.any是理想的选择。如果所有操作都失败,它会以一个特殊的错误类型AggregateError拒绝。

使用场景示例:

  • 从多个CDN镜像加载同一个资源文件,只要有一个成功加载就足够,以确保最快的响应速度和高可用性。
  • 向多个服务提供商发送请求以获取相同的数据,取第一个成功响应的数据。
  • 实现容错机制:尝试通过多种不同的策略执行一个操作,只要有一种策略成功即可。

2. 算法分析:短路机制与错误聚合

Promise.any的实现依赖于以下关键算法步骤:

  1. 创建返回Promise: Promise.any函数本身会立即返回一个新的Promise,我们称之为“主Promise”。
  2. 输入规范化与空数组处理:
    • 将输入的可迭代对象中的每个元素通过 Promise.resolve() 转换为一个真正的Promise。
    • 如果输入数组为空,主Promise会立即以一个AggregateError拒绝,因为没有Promise可以成功。
  3. 短路机制 (Short-circuiting on Fulfillment):
    • 这是Promise.any与众不同之处。一旦任何一个输入的Promise成功(Fulfilled),主Promise就会立即以该成功Promise的值进行解析。此时,它会忽略所有其他尚未结算或已拒绝的Promise。
  4. 错误聚合 (AggregateError on All Rejection):
    • 如果所有输入的Promise都拒绝(Rejected),主Promise才会拒绝。
    • 此时,它不会简单地拒绝第一个拒绝的Promise的理由,而是会收集所有拒绝的理由,并将它们包装在一个AggregateError实例中进行拒绝。AggregateError是一个特殊的错误,它包含一个errors属性,该属性是一个数组,存储了所有被拒绝的Promise的理由。
  5. 状态追踪机制:
    • 初始化一个数组来存储所有拒绝的理由(reasons)。
    • 初始化一个计数器,用于跟踪已拒绝 (rejected) 的Promise数量。
    • 一个布尔标志,例如 hasFulfilled,用于在第一个Promise成功时进行短路。

3. 详细算法步骤与伪代码

  1. 函数签名: myAny(promises)
  2. 参数检查: 确保promises是一个可迭代对象。
  3. 初始化:
    • reasons = new Array(promises.length)
    • rejectedCount = 0
    • totalPromises = promises.length
    • hasFulfilled = false (用于短路)
  4. 空数组处理:
    • 如果 totalPromises === 0,则立即返回 Promise.reject(new AggregateError([], 'All promises were rejected'))
  5. 创建并返回主Promise:
    • return new Promise((resolve, reject) => { ... })
  6. 迭代与监听:
    • 对于 promises 数组中的每个 promise (及其索引 index):
      • Promise.resolve(promise) 将其标准化为Promise p
      • p.then(value => { ... })
        • 短路逻辑: 如果 !hasFulfilled (即主Promise尚未被解析),则:
          • hasFulfilled = true
          • resolve(value) (立即解析主Promise)
      • p.catch(reason => { ... })
        • 错误聚合逻辑: 如果 !hasFulfilled (即主Promise尚未被解析,因为如果已解析,拒绝就无关紧要了):
          • reasons[index] = reason (记录拒绝理由)
          • rejectedCount++
          • checkAllRejected()
  7. 完成检查函数 checkAllRejected()
    • 如果 rejectedCount === totalPromises,并且 !hasFulfilled (确保没有Promise已经成功解析了主Promise),则:
      • reject(new AggregateError(reasons, 'All promises were rejected'))

4. 代码实现 (myAny)

/**
 * 模拟实现 Promise.any
 * @param {Array<Promise<any> | any>} promises - 一个Promise或值的可迭代对象
 * @returns {Promise<any>}
 */
function myAny(promises) {
    // 1. 确保输入是一个可迭代对象,并将其转换为数组
    const promiseArray = Array.isArray(promises) ? promises : [...promises];
    const totalPromises = promiseArray.length;

    // 2. 如果输入数组为空,立即返回一个以AggregateError拒绝的Promise
    if (totalPromises === 0) {
        return Promise.reject(new AggregateError([], 'All promises were rejected'));
    }

    // 3. 创建一个数组来存储所有拒绝的理由,以备所有Promise都拒绝时使用
    const reasons = new Array(totalPromises);
    // 4. 初始化一个计数器,用于跟踪已拒绝的Promise数量
    let rejectedCount = 0;
    // 5. 标志位,用于在第一个Promise成功时短路主Promise的解析
    let hasFulfilled = false;

    // 6. 返回一个新的Promise
    return new Promise((resolve, reject) => {
        // 遍历每个输入的Promise或值
        promiseArray.forEach((p, index) => {
            // 7. 将当前项标准化为Promise
            Promise.resolve(p)
                .then(value => {
                    // 8. 如果主Promise尚未被解析(即没有其他Promise已经成功)
                    if (!hasFulfilled) {
                        hasFulfilled = true; // 标记已成功
                        resolve(value);       // 立即解析主Promise
                    }
                })
                .catch(reason => {
                    // 9. 如果主Promise尚未被解析(即没有Promise成功),才处理拒绝
                    if (!hasFulfilled) {
                        reasons[index] = reason; // 记录拒绝理由
                        rejectedCount++;         // 增加拒绝计数器

                        // 10. 检查是否所有Promise都已拒绝
                        if (rejectedCount === totalPromises) {
                            // 11. 如果所有Promise都已拒绝,且没有Promise成功,则拒绝主Promise
                            //     使用 AggregateError 包装所有拒绝理由
                            reject(new AggregateError(reasons, 'All promises were rejected'));
                        }
                    }
                });
        });
    });
}

// 示例用法
const pA = new Promise((_, reject) => setTimeout(() => reject('Error A'), 100));
const pB = new Promise((resolve) => setTimeout(() => resolve('Success B'), 50));
const pC = new Promise((_, reject) => setTimeout(() => reject('Error C'), 150));
const pD = Promise.resolve('Success D');

myAny([pA, pB, pC, pD])
    .then(result => {
        console.log("myAny Result (first success):", result); // 可能是 'Success D' 或 'Success B' (取决于宏任务调度)
    })
    .catch(error => {
        console.error("myAny Error:", error);
    });

// 示例:所有Promise都拒绝
const pE = new Promise((_, reject) => setTimeout(() => reject('Fail E'), 30));
const pF = new Promise((_, reject) => setTimeout(() => reject('Fail F'), 60));

myAny([pE, pF])
    .then(result => {
        console.log("myAny Result (should not happen):", result);
    })
    .catch(error => {
        console.error("myAny Error (all rejected):", error);
        console.error("  Error type:", error.constructor.name); // AggregateError
        console.error("  Rejection reasons:", error.errors); // ['Fail E', 'Fail F']
    });

myAny([])
    .catch(error => {
        console.error("myAny with empty array error:", error);
        console.error("  Error type:", error.constructor.name); // AggregateError
    });

5. 状态追踪的表格表示

同样,我们用表格来追踪myAny的执行。假设有三个Promise:P_1 (reject ‘E1’),P_2 (resolve ‘S2’),P_3 (reject ‘E3’)。

事件发生 P_1 状态 P_2 状态 P_3 状态 reasons 数组 (简化) rejectedCount hasFulfilled 主Promise状态
初始化 Pending Pending Pending [undefined, undefined, undefined] 0 false Pending
P_1 拒绝 Rejected Pending Pending [E1, undefined, undefined] 1 false Pending
P_2 解决 Rejected Fulfilled Pending [E1, undefined, undefined] 1 true Resolved
P_3 拒绝 Rejected Fulfilled Rejected [E1, undefined, E3] 2 true Resolved
检查完成 (无关) (无关) (无关) (无关) (无关) true Resolved

注意在P_2解决后,hasFulfilled变为true,主Promise立即解析。后续P_3的拒绝将不再影响主Promise的状态。

如果所有Promise都拒绝,例如:P_X (reject ‘X’), P_Y (reject ‘Y’)

事件发生 P_X 状态 P_Y 状态 reasons 数组 (简化) rejectedCount hasFulfilled 主Promise状态
初始化 Pending Pending [undefined, undefined] 0 false Pending
P_X 拒绝 Rejected Pending [X, undefined] 1 false Pending
P_Y 拒绝 Rejected Rejected [X, Y] 2 false Pending
检查完成 Rejected Rejected [X, Y] 2 (== total) false Rejected (AggregateError)

在这种情况下,rejectedCount达到totalPromiseshasFulfilled仍为false,触发主Promise以AggregateError拒绝。

Promise.allSettledPromise.any 的比较分析

虽然Promise.allSettledPromise.any都用于处理多个Promise,但它们的语义、行为和应用场景截然不同。理解这些差异对于选择正确的组合器至关重要。

特性 / 组合器 Promise.allSettled Promise.any
主要目的 获取所有Promise的最终状态(无论成功或失败),不短路。 只要有一个Promise成功就立即成功,所有失败才失败。
解析条件 所有输入的Promise都已结算(Fulfilled或Rejected)。 任何一个输入的Promise被Fulfilled
拒绝条件 永不拒绝,总是解析为一个状态数组。 所有输入的Promise都被Rejected
解析值 一个数组,包含每个Promise的结果对象 {status, value/reason} 第一个Fulfilled的Promise的值。
拒绝值 不会拒绝。 一个AggregateError实例,包含所有拒绝的理由。
短路行为 无短路。等待所有Promise完成。 在第一个Promise成功时短路
使用场景 批量操作结果汇总、错误报告、显示完整加载状态。 竞速获取数据、多源冗余、容错操作。

共享机制:

尽管行为不同,二者在底层实现上共享一些基本机制:

  • Promise.resolve(): 都用于将输入数组中的非Promise值标准化为Promise,确保统一处理。
  • 微任务调度: Promise回调的执行都依赖于微任务队列,保证异步操作的非阻塞和高效。
  • 主Promise模式: 都返回一个新的Promise,这个Promise的命运由内部逻辑协调。
  • 状态追踪: 都使用内部计数器和数组来追踪输入Promise的进度和结果。

差异逻辑:

  • allSettled的核心是“等待”,它不关心中间的成功或失败,只关注所有操作都完成后,才能给出最终的完整报告。
  • any的核心是“竞速”,它渴望最快的成功,并愿意为此放弃其他仍在进行或已失败的操作。同时,它对“完全失败”的定义是所有路径都失败,并提供一个聚合的错误报告。

性能与最佳实践

1. 性能考量

  • 资源消耗: 这两个组合器都需要为每个输入的Promise附加至少一个回调链(then/catch/finally)。对于非常大的Promise数组,这会增加内存和微任务队列的负担。然而,对于大多数实际应用场景,这种开销是可接受的。
  • 短路优势: Promise.any的短路机制使其在某些场景下比Promise.allSettledPromise.all更高效。一旦达到成功条件,它就不再关心其他Promise的执行,可以节省不必要的计算和网络资源。

2. 错误处理

  • Promise.allSettled的错误处理: 由于它永不拒绝,所有的错误都会作为{status: 'rejected', reason: ...}对象包含在解析结果中。这意味着你必须在.then()回调中遍历结果数组,检查每个对象的status来处理错误。
  • Promise.any的错误处理: 它通过AggregateError提供了一个清晰的错误信号,如果所有Promise都失败,你可以在.catch()块中统一处理。AggregateError.errors属性允许你访问所有具体的拒绝理由,进行细粒度的错误报告或重试逻辑。

3. 选择正确的组合器

  • Promise.all 当你需要所有Promise都成功,并且任何一个失败都意味着整个操作失败时。
  • Promise.race 当你只需要第一个结算(成功或失败)的Promise结果时。
  • Promise.allSettled 当你需要知道所有Promise的最终状态,无论成功与否,且不希望任何失败导致整个组合器拒绝时。
  • Promise.any 当你只需要一个Promise成功,并且如果所有都失败时,才视为整个操作失败并需要所有失败理由时。

结语

通过本次讲座,我们深入剖析了Promise.allSettledPromise.any这两个强大的Promise组合器的底层实现原理。它们都巧妙地利用了JavaScript的Promise机制和微任务队列,通过精细的状态追踪和异步协调,为我们提供了处理复杂并发场景的利器。理解它们各自的算法、短路逻辑和错误处理机制,能够帮助我们更准确地选择和应用这些工具,从而构建出更健壮、更高效的异步JavaScript应用程序。

发表回复

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