- Published on
Promise.all:从规范到手写实现
Promise.all 的规范与要求
根据 ECMAScript 规范,一个健壮的 Promise.all 实现必须满足以下核心要求:
Promise.all 的核心规范- 返回 Promise:
Promise.all函数必须返回一个新的Promise实例。 - 接受可迭代对象: 参数必须是一个可迭代对象 (iterable)(如
Array,Map,Set, 字符串等),其成员可以是 Promise 或非 Promise 值。 - 参数校验: 如果传入的参数不是一个可迭代对象,必须立即返回一个
rejected状态的 Promise,并附带一个TypeError。 - 空迭代器处理: 如果传入的可迭代对象为空,必须立即返回一个
fulfilled状态的 Promise,其值为一个空数组[]。 - 成功条件: 只有当所有输入的 Promise 都变为
fulfilled时,返回的 Promise 才会fulfilled。其成功的值是一个数组,包含了所有输入 Promise 的结果,且顺序与输入时完全一致。 - 失败条件: 只要有任意一个输入的 Promise 变为
rejected,返回的 Promise 就会立即rejected,其失败的原因就是第一个失败的 Promise 的原因。
Promise.all 的 Polyfill 实现
基于上述规范,我们可以构建一个 Promise.all 的 polyfill。这个过程的核心在于如何正确地追踪所有 Promise 的状态、维持结果的顺序以及处理“快速失败”的逻辑。
健壮的
Promise.all 实现function promiseAll(iterable) {
return new Promise((resolve, reject) => {
// 1. 验证参数是否为可迭代对象
if (iterable == null || typeof iterable[Symbol.iterator] !== 'function') {
return reject(new TypeError('Argument is not iterable.'));
}
const items = Array.from(iterable);
const itemCount = items.length;
// 2. 处理空迭代器的边界情况
if (itemCount === 0) {
return resolve([]);
}
const results = new Array(itemCount);
let pendingCount = itemCount;
let hasRejected = false;
// 3. 遍历所有项目
items.forEach((item, index) => {
// 使用 Promise.resolve 包装,以统一处理 Promise 和非 Promise 值
Promise.resolve(item).then(
// onFulfilled 回调
(value) => {
// 如果已有其他 Promise 失败,则忽略此成功结果
if (hasRejected) return;
// 将结果按原始顺序存放在结果数组的对应位置
results[index] = value;
pendingCount--;
// 如果所有 Promise 都已成功,则 resolve 最终的 Promise
if (pendingCount === 0) {
resolve(results);
}
},
// onRejected 回调
(reason) => {
// 确保只 reject 一次(“快速失败”机制)
if (hasRejected) return;
hasRejected = true;
// 立即 reject 最终的 Promise
reject(reason);
}
);
});
});
}
关键行为深度解析
结果的顺序保证
Promise.all 的一个核心特性是,无论内部的 Promise 以何种顺序完成,最终成功时返回的结果数组,其顺序都与传入的可迭代对象的顺序严格一致。
实现原理
在实现中,我们首先根据输入项的数量,创建一个固定长度的、空的 results 数组。当每个 Promise 完成时,我们是根据它在原始数组中的索引 (index),将结果直接放入 results 数组的对应位置,而不是 push 进去。这确保了最终结果的顺序性。
“快速失败 (Fail-Fast)” 与不可中止的异步操作
Promise.all 不会中止其他操作当 Promise.all 因为一个 Promise 失败而立即 reject 时,它只是停止了对其他 Promise 结果的等待。
它没有能力,也不会去中止那些已经在后台运行的其他异步操作(例如,其他未完成的 fetch 请求)。这些请求会继续在后台运行,直到它们完成或失败。只是它们最终的结果,会被 Promise.all 完全忽略。在需要进行资源清理的场景下(如中止 fetch),应在每个独立的 Promise 中结合 AbortController 来实现。