手写 Promise.all:当其中一个 reject 时,如何优雅处理剩余请求?
大家好,欢迎来到今天的专题讲座。今天我们不讲“Hello World”,也不讲“闭包”或“原型链”。我们来聊聊一个看似简单、实则暗藏玄机的 JavaScript 高阶特性 —— Promise.all。
你可能已经用过 Promise.all([...]),比如并发发起多个 API 请求,等它们都成功后再统一处理结果。但如果你只停留在“它能跑起来”的层面,那今天的内容将彻底改变你的认知。
一、什么是 Promise.all?它的默认行为是什么?
先从基础开始。Promise.all 是 ES6 引入的一个静态方法,用于并行执行一组 Promise,并在所有 Promise 成功完成时返回一个包含所有结果的数组;如果其中任意一个 Promise 失败(reject),整个 Promise.all 就会立即失败(reject),不会等待其他 Promise 完成。
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.reject(new Error('something went wrong'));
Promise.all([p1, p2, p3])
.then(results => console.log(results))
.catch(err => console.error(err.message));
// 输出: "something went wrong"
⚠️ 注意:即使 p1 和 p2 已经 resolve 了,也不会被收集到最终的结果中!因为一旦有任一 reject,整个流程就被终止了。
这正是问题的核心所在:我们真的希望这样吗?
现实中很多场景下,我们并不想因为一个失败就放弃全部数据。比如:
- 后台同时调用多个接口获取用户信息;
- 其中某个接口超时/报错,但我们仍希望保留其他接口的成功数据;
- 最终展示时可以标记哪些字段失败,而不是直接中断整个页面渲染。
所以我们要做的不是“照搬原生实现”,而是理解其底层逻辑后,自定义一个更健壮的版本。
二、手写 Promise.all 的核心思路
要实现一个“容错版”的 Promise.all,我们需要:
- 并行运行所有 Promise
- 记录每个 Promise 的状态(resolve/reject)和值
- 无论是否出错,都要等到所有 Promise 结束才决定最终结果
- 返回结构化的结果数组,包含成功与失败的信息
这就是所谓的 “fail-fast” vs “fail-silent” 的区别。
✅ 正确做法:使用 Promise.allSettled(现代方案)
ES2020 引入了 Promise.allSettled,专门解决这个问题:
const promises = [
Promise.resolve(1),
Promise.reject(new Error("bad")),
Promise.resolve(3)
];
Promise.allSettled(promises).then(results => {
console.log(results.map(r => r.status === 'fulfilled' ? r.value : r.reason));
});
// 输出: [1, Error: bad, 3]
✅ 它不会提前终止,而是等所有 promise settle 后再统一返回。
👉 所以如果你只需要兼容性好的解决方案,直接用 Promise.allSettled 即可!
但如果我们是在面试、教学或深入理解原理呢?那就必须手动实现一遍。
三、手写 Promise.all 的完整实现(支持 fail-safe)
下面是一个完整的、带注释的手写版本,模拟原生 Promise.all 的行为,但允许部分失败而不中断整体流程:
function myPromiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument must be an array'));
}
if (promises.length === 0) {
return resolve([]);
}
const results = new Array(promises.length);
let resolvedCount = 0;
let rejected = false;
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then(value => {
if (!rejected) {
results[index] = { status: 'fulfilled', value };
resolvedCount++;
if (resolvedCount === promises.length) {
resolve(results);
}
}
})
.catch(reason => {
if (!rejected) {
rejected = true;
results[index] = { status: 'rejected', reason };
// 不立即 reject,继续等待其他 Promise 完成
// 等待所有完成后统一 resolve
}
});
});
});
}
🔍 关键点解析:
| 特性 | 实现方式 | 目的 |
|---|---|---|
| 并发执行 | 使用 forEach + Promise.resolve() |
确保异步并行 |
| 每个结果独立存储 | results[index] |
保持顺序,避免混乱 |
| 错误不中断流程 | if (!rejected) 控制 |
允许部分失败 |
| 统一完成判断 | resolvedCount === length |
确保所有任务结束才返回 |
🧪 测试案例:
const p1 = Promise.resolve(1);
const p2 = Promise.reject(new Error('p2 failed'));
const p3 = Promise.resolve(3);
myPromiseAll([p1, p2, p3]).then(res => {
console.log(res);
});
// 输出:
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: Error: p2 failed },
// { status: 'fulfilled', value: 3 }
// ]
💡 这样你就拿到了每一步的真实状态,完全可以根据这个结构做进一步处理(例如 UI 展示错误提示、跳过失败项等)。
四、对比:原生 vs 自定义 vs allSettled
| 方案 | 是否中断 | 是否收集全部结果 | 是否推荐 |
|---|---|---|---|
Promise.all |
✅ 是 | ❌ 否(只返回前几个) | ⚠️ 仅适用于强一致性要求 |
myPromiseAll(本文实现) |
❌ 否 | ✅ 是 | ✅ 推荐用于大多数业务场景 |
Promise.allSettled |
❌ 否 | ✅ 是 | ✅ 最佳实践(标准 API) |
💬 补充说明:
- 如果你项目已支持 ES2020+,请优先使用
Promise.allSettled。- 如果你在老环境中(如 Node.js < 12),或者想学习底层机制,建议手写
myPromiseAll。
五、常见误区 & 常见陷阱
❗ 误区一:认为 Promise.all 只是简单的并行执行
很多人以为 Promise.all 就是把多个 Promise 放进数组里自动并发执行,其实不然。它的关键在于“同步控制”——一旦第一个 reject 出现,就立刻停止后续任务。
const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 1000));
const fast = Promise.resolve('fast');
Promise.all([fast, slow]).then(console.log);
// 输出: ['fast', 'slow'] —— 但它其实等了整整 1 秒!
❌ 错误理解:以为它只是“并行”,其实是“串行等待最慢的那个”。
✅ 正确认识:Promise.all 是“等待所有完成”,而不是“同时触发然后马上返回”。
❗ 误区二:试图用 Promise.race 替代 all
// ❌ 错误示范
Promise.race([p1, p2, p3]).then(result => console.log(result));
// 只取第一个完成的,不管成功还是失败
这不是你要的。race 是用来选最快的,而你是想收集全部状态。
六、真实应用场景举例
场景 1:多接口加载用户资料(容忍个别失败)
假设前端需要同时请求以下三个接口:
/api/user/profile/api/user/avatar/api/user/settings
有些接口可能会因网络波动或权限限制失败,但我们仍希望显示已加载的部分内容。
使用我们的 myPromiseAll 或 allSettled:
const requests = [
fetch('/api/user/profile').then(r => r.json()),
fetch('/api/user/avatar').then(r => r.json()),
fetch('/api/user/settings').then(r => r.json())
];
myPromiseAll(requests).then(results => {
const profile = results[0].value || {};
const avatar = results[1].value || null;
const settings = results[2].value || {};
renderUser(profile, avatar, settings);
});
这样即使某个接口失败,也不会影响整体页面渲染,用户体验更好。
场景 2:批量上传文件(逐个失败不影响其余)
const uploads = files.map(file => uploadFile(file));
myPromiseAll(uploads).then(results => {
const success = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
showNotification(`上传完成:${success.length} 成功,${failed.length} 失败`);
});
这种设计非常适合上传进度条、错误日志追踪等复杂交互。
七、性能优化建议(高级技巧)
虽然上面的实现已经很清晰,但在高并发场景下(比如 1000+ 个 Promise),你可以考虑以下优化:
✅ 分批处理(chunking)
避免一次性创建太多 pending Promise 导致内存爆炸:
function myPromiseAllChunked(promises, chunkSize = 50) {
const chunks = [];
for (let i = 0; i < promises.length; i += chunkSize) {
chunks.push(promises.slice(i, i + chunkSize));
}
return Promise.all(chunks.map(chunk => myPromiseAll(chunk)))
.then(flattened => flattened.flat());
}
这种方式适合极端情况下的大规模 Promise 并发,提升稳定性和响应速度。
八、总结:你该怎么做?
| 你的情况 | 推荐方案 |
|---|---|
| 新项目 / 现代浏览器环境 | ✅ 使用 Promise.allSettled(最简洁可靠) |
| 学习原理 / 面试准备 | ✅ 手写 myPromiseAll(加深理解) |
| 老项目兼容旧环境 | ✅ 手写 myPromiseAll(可控性强) |
| 需要分批处理大量任务 | ✅ 加入 chunk 分片逻辑 |
📌 核心结论:
Promise.all的默认行为并不是“最佳实践”,而是一种“严格一致”的策略。
在实际开发中,我们应该主动选择是否容忍部分失败,从而构建更具鲁棒性的系统。
附录:完整代码参考(可复制粘贴)
function myPromiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument must be an array'));
}
if (promises.length === 0) {
return resolve([]);
}
const results = new Array(promises.length);
let resolvedCount = 0;
let rejected = false;
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then(value => {
if (!rejected) {
results[index] = { status: 'fulfilled', value };
resolvedCount++;
if (resolvedCount === promises.length) {
resolve(results);
}
}
})
.catch(reason => {
if (!rejected) {
rejected = true;
results[index] = { status: 'rejected', reason };
}
});
});
});
}
// 测试
const p1 = Promise.resolve(1);
const p2 = Promise.reject(new Error('test error'));
const p3 = Promise.resolve(3);
myPromiseAll([p1, p2, p3]).then(console.log);
希望这篇讲解不仅帮你搞懂了 Promise.all 的本质,也让你明白:真正的高手,不只是知道怎么用 API,更是懂得为什么这么设计,以及如何根据业务需求做出调整。
下次遇到类似问题时,请记住一句话:
“不要盲目依赖默认行为,要学会问:‘我想要的是什么?’”
谢谢大家!