手写 `Promise.all`:如果其中一个 reject 了,如何处理剩余的请求?

手写 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,我们需要:

  1. 并行运行所有 Promise
  2. 记录每个 Promise 的状态(resolve/reject)和值
  3. 无论是否出错,都要等到所有 Promise 结束才决定最终结果
  4. 返回结构化的结果数组,包含成功与失败的信息

这就是所谓的 “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

有些接口可能会因网络波动或权限限制失败,但我们仍希望显示已加载的部分内容。

使用我们的 myPromiseAllallSettled

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,更是懂得为什么这么设计,以及如何根据业务需求做出调整。

下次遇到类似问题时,请记住一句话:

“不要盲目依赖默认行为,要学会问:‘我想要的是什么?’”

谢谢大家!

发表回复

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