手写 Promise.all 的原理与实现
一、引言
Promise.all
是 JavaScript 异步编程中处理并发任务的重要 API。它接收一个可迭代对象,返回一个新的 Promise。当所有输入 Promise 都成功时,新 Promise 以结果数组的形式成功;当任一输入 Promise 失败时,新 Promise 立即失败并返回该错误。 本文将从规范出发,剖析其底层运行机制,并给出两种实现方案及对比分析。
二、规范与特性
根据 ECMMAScript 规范,Promise.all
具有以下特性:
-
输入类型
必须是可迭代对象(Array、Set 等实现了Symbol.iterator
接口的对象)。若输入非可迭代对象,应抛出TypeError
。 -
空输入处理
若输入为空可迭代对象,应立即返回已成功状态的 Promise,结果为空数组。 -
成功条件
所有 Promise 成功后,返回一个按输入顺序排列的结果数组。 -
失败条件
只要有一个 Promise 失败,Promise.all
立即以该原因失败,忽略其他未完成 Promise 的结果。
三、实现思路
1. 输入校验
通过检测 Symbol.iterator
方法判断是否为可迭代对象:
if (typeof iterable[Symbol.iterator] !== 'function') {return Promise.reject(new TypeError('The input is not iterable'));
}
2. 空输入处理
将输入转换为数组,若长度为 0,直接返回成功状态:
const promises = Array.from(iterable);
if (promises.length === 0) {return Promise.resolve([]);
}
3. 异步任务管理
使用一个数组存储结果,一个计数器统计已完成的 Promise 数量:
- 每个 Promise 成功时,将结果存入对应索引位置,计数器加一;
- 当计数器等于总任务数时,调用
resolve
返回结果数组; - 任何一个 Promise 失败时,直接调用
reject
抛出错误。
核心逻辑:
return new Promise((resolve, reject) => {const results = new Array(promises.length);let resolvedCount = 0;for (let i = 0; i < promises.length; i++) {Promise.resolve(promises[i]).then(value => {results[i] = value;resolvedCount++;if (resolvedCount === promises.length) {resolve(results);}}).catch(reject);}
});
四、两种实现方案对比
方案一:外部保存 resolve/reject
Promise.myAll = function (iterable) {if (typeof iterable[Symbol.iterator] !== 'function') {return Promise.reject(new TypeError('The input is not iterable'));}let resolver, rejecter;const p = new Promise((resolve, reject) => {resolver = resolve;rejecter = reject;});const promises = Array.from(iterable);const n = promises.length;if (n === 0) {resolver([]);return p;}const results = new Array(n);let resolvedCount = 0;for (let i = 0; i < n; i++) {Promise.resolve(promises[i]).then(value => {results[i] = value;resolvedCount++;if (resolvedCount === n) {resolver(results);}}).catch(err => rejecter(err));}return p;
};
特点:
- 将
resolve
/reject
暴露到外部变量; - 逻辑拆分清晰,但存在变量暴露风险。
方案二:内部封装 resolve/reject
Promise.myAll = function (iterable) {if (typeof iterable[Symbol.iterator] !== 'function') {return Promise.reject(new TypeError('The input is not iterable'));}return new Promise((resolve, reject) => {const promises = Array.from(iterable);const n = promises.length;if (n === 0) {return resolve([]);}const results = new Array(n);let resolvedCount = 0;for (let i = 0; i < n; i++) {Promise.resolve(promises[i]).then(value => {results[i] = value;resolvedCount++;if (resolvedCount === n) {resolve(results);}}).catch(reject);}});
};
特点:
- 状态控制函数封装在 Promise 内部,安全性更高;
- 代码简洁,符合最小权限原则。
五、测试验证
为确保实现符合规范,需覆盖以下测试场景:
-
空输入
Promise.myAll([]).then(res => console.log('空输入:', res)); // []
-
全部成功(含非 Promise 值)
Promise.myAll([Promise.resolve(1), 2, 3]).then(res => console.log('全部成功:', res)); // [1, 2, 3]
-
任一失败
Promise.myAll([Promise.resolve(1), Promise.reject('error'), Promise.resolve(3)]).catch(err => console.error('失败原因:', err)); // error
-
非可迭代输入
Promise.myAll(123).catch(err => console.error('类型错误:', err.message)); // The input is not iterable
六、常见问题与注意事项
-
非 Promise 输入处理
必须使用Promise.resolve
将输入值统一转换为 Promise,否则非 Promise 值会导致then
调用报错。 -
结果顺序保证
使用固定长度数组按索引存储结果,确保输出顺序与输入顺序一致。 -
快速失败机制
利用 Promise 状态不可逆特性,一旦 reject 后,后续的 resolve 或 reject 都不会改变最终状态。
七、结论
Promise.all
的本质是一个并发任务管理器,通过统一输入处理、顺序结果收集和快速失败机制,实现了对批量异步任务的高效管理。手写实现不仅有助于理解其底层原理,也能在实际开发中更好地运用该 API。在实现方式上,推荐使用内部封装 resolve
/reject
的写法,以提高代码安全性与简洁性。