Skip to content

Promise 组合器

基于现代 JavaScript(ES2025 · 核于 2026-06)

速查

  • 四个静态组合器:把「一批 Promise」聚成「一个 Promise」,输入可迭代(数组等)
  • Promise.all(iterable)全部兑现才兑现,结果是有序值数组任一拒绝即整体拒绝(fail-fast,快速失败)
  • Promise.allSettled(iterable)等全部敲定永不拒绝;结果是 {status, value | reason} 对象数组
  • Promise.race(iterable)第一个敲定的(无论兑现还是拒绝)决定结果——谁先到听谁的
  • Promise.any(iterable)第一个兑现的决定结果;全部拒绝才拒绝,原因是 AggregateError
  • 选择口诀:要全部成功且 fail-fast → all;要全部结果不怕失败 → allSettled;要最快的成功 → any;要最快敲定(含失败,多用于超时)→ race
  • 顺序保证all / allSettled 返回数组的顺序按输入顺序,与谁先完成无关
  • 并发控制:组合器会同时发起全部任务;要限制并发量(如最多 5 个)需手动分批或用任务池

为什么需要组合器

单个 Promise 只表示一件异步事。但真实场景常常要同时处理一批:并行拉取多个接口、从多个镜像里取最快的、批量上传后统计成功率……这时就要把「多个 Promise」组合成「一个 Promise」来统一等待。ES 提供四个静态方法,区别全在于**「什么时候敲定、敲定成什么」**。

下面统一用这组辅助函数演示:

js
// 延迟后兑现
const ok = (val, ms) => new Promise((res) => setTimeout(() => res(val), ms));
// 延迟后拒绝
const fail = (msg, ms) => new Promise((_, rej) => setTimeout(() => rej(new Error(msg)), ms));

Promise.all:全部成功,否则全败

Promise.all所有输入 Promise 兑现,然后兑现为一个按输入顺序排列的值数组;只要有一个拒绝,它立刻以那个原因拒绝——这叫快速失败(fail-fast)

js
const [a, b, c] = await Promise.all([ok("A", 100), ok("B", 50), ok("C", 80)]);
console.log(a, b, c); // "A" "B" "C"——顺序按输入,不按完成快慢

拒绝时的行为要特别注意:

js
try {
  await Promise.all([ok("A", 100), fail("B 挂了", 30), ok("C", 80)]);
} catch (err) {
  console.error(err.message); // "B 挂了"——最早拒绝的那个
}
// ⚠️ A 和 C 的请求并不会被取消,它们仍在后台跑完,只是结果被丢弃

适用场景:多个请求缺一不可、且希望「一个失败就别等了」——比如渲染页面需要的用户信息、配置、权限三个接口必须都成功。

all 不会取消其余任务

Promise.all 拒绝只是让返回的那个 Promise 进入拒绝态,已发起的其他异步操作不会被中止(Promise 没有内建取消)。真要取消,得配合 AbortController,见 取消、超时与竞态

Promise.allSettled:要全部结果,不怕失败

Promise.allSettled所有输入 Promise 敲定(无论兑现还是拒绝),然后永不拒绝地兑现为一个结果对象数组。每个元素形如:

  • 兑现:{ status: "fulfilled", value: 值 }
  • 拒绝:{ status: "rejected", reason: 原因 }
js
const results = await Promise.allSettled([
  ok("A", 100),
  fail("B 挂了", 30),
  ok("C", 80),
]);
console.log(results);
// [
//   { status: "fulfilled", value: "A" },
//   { status: "rejected",  reason: Error("B 挂了") },
//   { status: "fulfilled", value: "C" },
// ]

// 分拣成功与失败
const ok2 = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
const bad = results.filter((r) => r.status === "rejected").map((r) => r.reason);

适用场景:批量操作里部分失败可接受,且你想知道每一个的结局——比如批量上传 10 个文件、统计「8 成 2 败」并分别提示;或并行请求多个互不依赖的看板数据,挂掉一两个不影响其余渲染。

Promise.race:谁先敲定听谁的

Promise.race第一个输入 Promise 敲定时立刻随之敲定——不管它是兑现还是拒绝。后到的全部忽略。

js
const winner = await Promise.race([ok("慢", 200), ok("快", 50)]);
console.log(winner); // "快"——先敲定的赢

// 第一个敲定的若是拒绝,race 就拒绝
try {
  await Promise.race([ok("数据", 200), fail("先炸了", 50)]);
} catch (err) {
  console.error(err.message); // "先炸了"
}

经典用法是做超时——让真实任务和一个「定时拒绝」赛跑:

js
function withTimeout(promise, ms) {
  const timeout = new Promise((_, rej) =>
    setTimeout(() => rej(new Error(`超时 ${ms}ms`)), ms),
  );
  return Promise.race([promise, timeout]); // 任务没在 ms 内完成就以超时拒绝
}

try {
  const data = await withTimeout(fetch("/api/slow"), 3000);
} catch (err) {
  console.error(err.message); // 可能是 "超时 3000ms"
}

不过这种超时不会真正取消底层请求,更彻底的方案是 AbortSignal.timeout(见 取消、超时与竞态)。

Promise.any:第一个成功的就行

Promise.any第一个兑现时敲定(忽略在它之前的拒绝);只有全部都拒绝时才拒绝,且原因是一个聚合错误 AggregateError(其 .errors 数组收集所有拒绝原因)。

js
// 多个镜像源,取最快成功的那个
const fastest = await Promise.any([
  fail("CDN1 挂了", 30), // 拒绝被忽略
  ok("来自 CDN2", 80), // 第一个兑现 → any 兑现
  ok("来自 CDN3", 50),
]);
console.log(fastest); // "来自 CDN2"

// 全挂才拒绝
try {
  await Promise.any([fail("e1", 10), fail("e2", 20)]);
} catch (err) {
  console.log(err instanceof AggregateError); // true
  console.log(err.errors.map((e) => e.message)); // ["e1", "e2"]
}

适用场景:多个冗余源只要有一个成功就够——多 CDN / 多镜像取最快可用、多个备选 API 取首个返回的。

race 与 any 的关键区别

两者都「取最快」,但对失败的态度相反:

第一个敲定的是兑现第一个敲定的是拒绝
race兑现拒绝(失败也算「敲定」,赢了就听它的)
any兑现忽略,继续等下一个兑现

记忆:race 是「最快敲定」(含失败,适合超时),any 是「最快成功」(容忍失败,适合冗余源)。

四者对照表

组合器何时兑现何时拒绝结果形态
all全部兑现任一拒绝(fail-fast)有序值数组
allSettled全部敲定永不拒绝{status, value/reason} 数组
race第一个敲定是兑现第一个敲定是拒绝单个值
any第一个兑现全部拒绝(AggregateError单个值

并发控制:组合器会一次发起全部

四个组合器都会立刻同时发起数组里的所有任务。如果数组里有几百个请求,会瞬间打满连接、压垮服务器。需要限制并发量(如「最多同时 5 个」)时,组合器本身办不到,要手动分批或用「任务池」:

js
// 限制并发:每次最多跑 limit 个,跑完一个补一个
async function mapWithLimit(items, limit, task) {
  const results = new Array(items.length);
  let cursor = 0;

  async function worker() {
    while (cursor < items.length) {
      const i = cursor++; // 领取下一个任务索引
      results[i] = await task(items[i], i);
    }
  }

  // 启动 limit 个 worker 并行消费队列
  const workers = Array.from({ length: Math.min(limit, items.length) }, worker);
  await Promise.all(workers);
  return results;
}

// 用法:1000 个 URL,但同时最多 5 个请求在飞
const data = await mapWithLimit(urls, 5, (url) => fetch(url).then((r) => r.json()));

生产中也可直接用成熟库(如 p-limitp-map)来做并发限流,原理与上面一致。

小结

四个组合器把「一批 Promise」按不同策略收敛成「一个 Promise」:all 要么全胜要么速败,allSettled 收齐所有结局,race 抢最快敲定,any 抢最快成功;而限制并发需另写任务池。掌握了它们,再回头看 async/await 如何把这些组合写得更顺手,以及「顺序 vs 并行」的取舍:async/await