各位同仁,各位技术爱好者,大家好。
今天,我们将深入探讨JavaScript异步编程领域中两个功能强大且至关重要的Promise组合器:Promise.allSettled 和 Promise.any。它们为我们处理并发异步操作提供了精细的控制,但其底层实现机制,即如何基于异步序列与状态追踪来协同工作,才是我们本次讲座的核心。我们将从零开始,剖析它们的算法原理、实现细节,并辅以详尽的代码示例,力求揭示其内部运作的奥秘。
异步编程的基石:Promise与微任务队列
在深入探讨Promise.allSettled和Promise.any之前,我们必须先回顾Promise的基础及其在JavaScript运行时环境中的作用。Promise是处理异步操作结果的对象,它有三种状态:
- Pending (待定):初始状态,既不是成功也不是失败。
- Fulfilled (已成功):操作成功完成。
- Rejected (已失败):操作失败。
Promise的状态一旦从Pending变为Fulfilled或Rejected,就不可逆转。这个过程是异步的,其回调(通过.then()、.catch()或.finally()注册)被调度到微任务队列 (Microtask Queue)。
JavaScript的事件循环 (Event Loop) 是其并发模型的核心。它不断地检查调用栈是否为空,然后处理任务队列(宏任务,如setTimeout、setInterval、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的实现依赖于以下几个关键算法步骤:
- 创建返回Promise:
Promise.allSettled函数本身会立即返回一个新的Promise,我们称之为“主Promise”。这个主Promise的命运(解析或拒绝)将取决于所有输入Promise的最终状态。 - 输入规范化与空数组处理:
- 首先,它会将输入的可迭代对象(通常是数组)中的每个元素通过
Promise.resolve()转换为一个真正的Promise。这确保了即使输入数组中包含非Promise值,也能被正确处理。 - 如果输入数组为空,主Promise会立即以一个空数组进行解析。
- 首先,它会将输入的可迭代对象(通常是数组)中的每个元素通过
- 状态追踪机制:
- 初始化一个结果数组,其长度与输入Promise数组相同。每个位置将存储对应Promise的最终状态对象。
- 初始化一个计数器,用于跟踪已结算 (settled) 的Promise数量。一个Promise被认为是“结算”的,当它变为Fulfilled或Rejected状态时。
- 并行监听与结果记录:
- 遍历输入Promise数组。对于每一个Promise
p,我们都为其添加.then()和.catch()处理器(或者更简洁地使用.finally()来统一处理,但内部仍需区分成功与失败)。 - 核心逻辑:
- 无论
p是成功(Fulfilled)还是失败(Rejected),都会在对应的回调中记录其状态。 - 如果
p成功,记录{ status: 'fulfilled', value: <成功值> }到结果数组的对应位置。 - 如果
p失败,记录{ status: 'rejected', reason: <失败原因> }到结果数组的对应位置。 - 每当一个Promise结算,计数器加一。
- 无论
- 遍历输入Promise数组。对于每一个Promise
- 主Promise的解析:
- 在每个Promise的结算回调中,都会检查已结算的Promise数量是否等于输入Promise的总数。
- 一旦所有输入Promise都已结算,主Promise就会以完整的、包含所有状态对象的结果数组进行解析。
重要的是,Promise.allSettled不会因为任何一个Promise的拒绝而提前拒绝。它会耐心等待所有Promise都完成各自的使命。
3. 详细算法步骤与伪代码
为了更清晰地理解,我们可以将上述算法细化为以下步骤:
- 函数签名:
myAllSettled(promises) - 参数检查: 确保
promises是一个可迭代对象。 - 初始化:
results = new Array(promises.length)settledCount = 0totalPromises = promises.length
- 空数组处理:
- 如果
totalPromises === 0,则立即返回Promise.resolve([])。
- 如果
- 创建并返回主Promise:
return new Promise((resolve, reject) => { ... })
- 迭代与监听:
- 对于
promises数组中的每个promise(及其索引index):Promise.resolve(promise)将其标准化为Promisep。p.then(value => { ... })results[index] = { status: 'fulfilled', value: value }settledCount++checkCompletion()
p.catch(reason => { ... })results[index] = { status: 'rejected', reason: reason }settledCount++checkCompletion()
- 对于
- 完成检查函数
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.all和Promise.race都有所不同。它的核心思想是“只要有一个成功,我就成功”。如果所有Promise都失败,那么它才会失败。
1. Promise.any 的核心目的与使用场景
当我们需要并行执行多个异步操作,并且只要其中任何一个操作成功,我们就接受其结果,而忽略其他操作的成功或失败时,Promise.any是理想的选择。如果所有操作都失败,它会以一个特殊的错误类型AggregateError拒绝。
使用场景示例:
- 从多个CDN镜像加载同一个资源文件,只要有一个成功加载就足够,以确保最快的响应速度和高可用性。
- 向多个服务提供商发送请求以获取相同的数据,取第一个成功响应的数据。
- 实现容错机制:尝试通过多种不同的策略执行一个操作,只要有一种策略成功即可。
2. 算法分析:短路机制与错误聚合
Promise.any的实现依赖于以下关键算法步骤:
- 创建返回Promise:
Promise.any函数本身会立即返回一个新的Promise,我们称之为“主Promise”。 - 输入规范化与空数组处理:
- 将输入的可迭代对象中的每个元素通过
Promise.resolve()转换为一个真正的Promise。 - 如果输入数组为空,主Promise会立即以一个
AggregateError拒绝,因为没有Promise可以成功。
- 将输入的可迭代对象中的每个元素通过
- 短路机制 (Short-circuiting on Fulfillment):
- 这是
Promise.any与众不同之处。一旦任何一个输入的Promise成功(Fulfilled),主Promise就会立即以该成功Promise的值进行解析。此时,它会忽略所有其他尚未结算或已拒绝的Promise。
- 这是
- 错误聚合 (AggregateError on All Rejection):
- 如果所有输入的Promise都拒绝(Rejected),主Promise才会拒绝。
- 此时,它不会简单地拒绝第一个拒绝的Promise的理由,而是会收集所有拒绝的理由,并将它们包装在一个
AggregateError实例中进行拒绝。AggregateError是一个特殊的错误,它包含一个errors属性,该属性是一个数组,存储了所有被拒绝的Promise的理由。
- 状态追踪机制:
- 初始化一个数组来存储所有拒绝的理由(
reasons)。 - 初始化一个计数器,用于跟踪已拒绝 (rejected) 的Promise数量。
- 一个布尔标志,例如
hasFulfilled,用于在第一个Promise成功时进行短路。
- 初始化一个数组来存储所有拒绝的理由(
3. 详细算法步骤与伪代码
- 函数签名:
myAny(promises) - 参数检查: 确保
promises是一个可迭代对象。 - 初始化:
reasons = new Array(promises.length)rejectedCount = 0totalPromises = promises.lengthhasFulfilled = false(用于短路)
- 空数组处理:
- 如果
totalPromises === 0,则立即返回Promise.reject(new AggregateError([], 'All promises were rejected'))。
- 如果
- 创建并返回主Promise:
return new Promise((resolve, reject) => { ... })
- 迭代与监听:
- 对于
promises数组中的每个promise(及其索引index):Promise.resolve(promise)将其标准化为Promisep。p.then(value => { ... })- 短路逻辑: 如果
!hasFulfilled(即主Promise尚未被解析),则:hasFulfilled = trueresolve(value)(立即解析主Promise)
- 短路逻辑: 如果
p.catch(reason => { ... })- 错误聚合逻辑: 如果
!hasFulfilled(即主Promise尚未被解析,因为如果已解析,拒绝就无关紧要了):reasons[index] = reason(记录拒绝理由)rejectedCount++checkAllRejected()
- 错误聚合逻辑: 如果
- 对于
- 完成检查函数
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达到totalPromises且hasFulfilled仍为false,触发主Promise以AggregateError拒绝。
Promise.allSettled 与 Promise.any 的比较分析
虽然Promise.allSettled和Promise.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.allSettled或Promise.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.allSettled和Promise.any这两个强大的Promise组合器的底层实现原理。它们都巧妙地利用了JavaScript的Promise机制和微任务队列,通过精细的状态追踪和异步协调,为我们提供了处理复杂并发场景的利器。理解它们各自的算法、短路逻辑和错误处理机制,能够帮助我们更准确地选择和应用这些工具,从而构建出更健壮、更高效的异步JavaScript应用程序。