系列文章目录

《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)



前言

第 12 篇已从「手写 Promise」切入 async/await 与 LRU;本篇作为 Promise 专题,系统梳理三态与链式规则静态方法全家桶all / race / allSettled / any),以及工程里常见的超时、重试、并发池写法。面试高频表述题——「Promise 本身是同步的」——也会单独说明,并与第 08 篇宏任务/微任务对照。读完后应能根据场景选对 API,并用 async/await 写出可维护的异步流程。


一、Promise 核心机制(复习与深化)

1.1 三态与不可逆

状态 含义 可转移
pending 待定,初始态 → fulfilled / rejected
fulfilled 已成功 不可再变
rejected 已失败 不可再变

一旦 settled(fulfilled 或 rejected),状态不可回退;多次 resolve / reject 只有第一次生效。

1.2 then 必返回新 Promise

.then(onFulfilled, onRejected) 每次调用都返回全新的 Promise,这是链式调用的基础:

const p2 = Promise.resolve(1).then((x) => x + 1);
const p3 = p2.then((x) => x * 2);

p2.then(console.log); // 2
p3.then(console.log); // 4

规则摘要

  1. 回调返回普通值 → 下一个 then 收到该值(被 Promise.resolve 包装)。
  2. 回调返回 Promise / thenable → 下一个 then 等待其状态再决定 fulfilled/rejected。
  3. 回调 throw 或返回 rejected Promise → 后续 fulfilled 跳过,进入 rejected 链,直到被 catch 处理。

1.3 值穿透

onFulfilledonRejected 不是函数时,值会原样传递到下一个 then

Promise.resolve(42)
  .then(null, () => "ignored")
  .then((v) => console.log(v)); // 42

Promise.reject("err")
  .then((v) => v)
  .then(null, undefined) // 非函数,rejection 继续穿透
  .catch((e) => console.log(e)); // err

catch(fn) 等价 then(null, fn);若 catch 不抛错且返回普通值,后续 then 会回到 fulfilled 链

1.4 「Promise 本身是同步的」——面试怎么答

这句话不是说 Promise 没有异步,而是指:

  1. new Promise(executor) 里的 executor 函数同步执行resolve / reject 调用前的代码都在当前调用栈跑完)。
  2. .then / .catch / .finally 注册的回调才是微任务,在本轮同步代码结束后、下一个宏任务前执行(见第 08 篇)。
  3. async 函数被调用时,函数体执行到第一个 await 之前是同步的;await 之后的代码相当于 then 回调,属于微任务。
console.log("1");

new Promise((resolve) => {
  console.log("2"); // executor 同步
  resolve();
}).then(() => console.log("3")); // 微任务

async function f() {
  console.log("4"); // 同步
  await Promise.resolve();
  console.log("5"); // 微任务
}

f();
console.log("6");

// 1 → 2 → 4 → 6 → 3 → 5

标准答法:Promise 的创建与 executor 是同步的异步感来自 then 回调与 await 续体进入微任务队列


二、静态方法:all / race / allSettled / any

四个方法都接收 Iterable(常用数组),返回新的 Promise;差异在聚合语义

2.1 对照总表

方法 成功条件 失败条件 典型用途
Promise.all 全部 fulfilled 任一 rejected 多接口并行,缺一不可
Promise.race 最先 settle 的那个 最先 reject 则失败 超时、竞速
Promise.allSettled 全部 settle(无论成败) 不会 reject 批量任务要完整结果
Promise.any 任一 fulfilled 全部 rejected 多源备份,有一个成功即可

2.2 Promise.all

const delay = (ms, val) =>
  new Promise((r) => setTimeout(() => r(val), ms));

Promise.all([delay(100, "a"), delay(50, "b")]).then(console.log);
// ['a', 'b'] — 顺序与传入数组一致,与完成先后无关

Promise.all([
  Promise.resolve(1),
  Promise.reject(new Error("fail")),
])
  .then(console.log)
  .catch((e) => console.log(e.message)); // fail — 快速失败

要点

  • 元素会先经 Promise.resolve 包装。
  • 空数组 → 立即 resolve([])
  • 一个失败整体失败;其他 Promise 仍会各自执行完,但 all 的结果已是 rejected。

2.3 Promise.race

Promise.race([
  delay(200, "slow"),
  delay(50, "fast"),
]).then(console.log); // 'fast'

Promise.race([
  Promise.reject(new Error("err")),
  delay(1000, "ok"),
]).catch((e) => console.log(e.message)); // err — 先 reject 的先定结果

要点第一个 settled(fulfilled 或 rejected)决定返回值;其余任务不会被取消(fetch 等仍会继续,需 AbortController 才能真正中止)。

2.4 Promise.allSettled

Promise.allSettled([
  Promise.resolve(1),
  Promise.reject(new Error("x")),
  delay(10, 3),
]).then(console.log);
// [
//   { status: 'fulfilled', value: 1 },
//   { status: 'rejected', reason: Error: x },
//   { status: 'fulfilled', value: 3 }
// ]

要点永不 reject(除非 iterable 本身抛同步错);适合「批量上报、部分失败仍要汇总」的场景。

2.5 Promise.any

Promise.any([
  Promise.reject(new Error("a")),
  delay(30, "backup"),
  delay(100, "slow"),
]).then(console.log); // 'backup'

Promise.any([
  Promise.reject(new Error("a")),
  Promise.reject(new Error("b")),
]).catch((e) => console.log(e.errors.length)); // 2 — AggregateError

要点:与 all 相反——任一成功即成功全部失败才 reject,错误类型为 AggregateError.errors 为各 rejection 数组。

2.6 选型口诀

  • 都要成功all
  • 要比谁快race
  • 都要结果(含失败)allSettled
  • 有一个就行any

三、超时控制:race 模式

请求无内置超时时,常用 Promise.race([task, timeoutPromise])

const timeout = (ms, msg = "timeout") =>
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error(msg)), ms)
  );

const fetchWithTimeout = (url, ms = 5000) =>
  Promise.race([fetch(url), timeout(ms)]);

fetchWithTimeout("/api/data", 3000).catch((e) =>
  console.log(e.message)
);

注意

  1. 先 reject 的胜出——若请求在超时后完成,fetch 仍在进行,可能造成浪费;生产环境配合 AbortController 取消请求。
  2. 超时 Promise 不会自动取消 另一路;race 只决定谁先改变聚合 Promise 的状态
const fetchWithTimeout = (url, ms = 5000) => {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);
  return fetch(url, { signal: controller.signal }).finally(() =>
    clearTimeout(timer)
  );
};

四、重试:async/await 实现

接口偶发失败时,可在 async 函数里用 try/catch + 循环 重试:

const retry = async (fn, times = 3, delayMs = 0) => {
  for (let i = 0; i < times; i++) {
    try {
      return await fn();
    } catch (e) {
      if (i === times - 1) throw e;
      if (delayMs) await new Promise((r) => setTimeout(r, delayMs));
      console.warn(`retry ${i + 1}/${times}`);
    }
  }
};

let attempt = 0;
const unstable = async () => {
  if (++attempt < 3) throw new Error("fail");
  return "ok";
};

retry(unstable, 3).then(console.log); // ok — 第 3 次成功

变体

  • 指数退避delayMs * 2 ** i,减轻服务端压力。
  • 只重试特定错误catch 里判断 e.status === 503 再 continue。
  • race 组合:单次请求限时 + 整体重试次数上限。

五、并发池:限制同时进行的任务数

Promise.all(tasks.map(fn))同时发起全部请求;任务很多时需要并发上限(如同时最多 3 个):

const limitPool = async (tasks, limit = 2) => {
  const results = [];
  let index = 0;

  const worker = async () => {
    while (index < tasks.length) {
      const i = index++;
      results[i] = await tasks[i]();
    }
  };

  await Promise.all(
    Array.from({ length: Math.min(limit, tasks.length) }, worker)
  );
  return results;
};

const delay = (ms, v) =>
  new Promise((r) => setTimeout(() => r(v), ms));

const tasks = [1, 2, 3, 4, 5].map(
  (n) => () => delay(100, n).then((v) => {
    console.log("done", v, Date.now());
    return v;
  })
);

limitPool(tasks, 2).then(console.log);
// 同一时刻最多 2 个 "done",最终 [1,2,3,4,5]

思路:开 limit 个 worker,共享递增下标 index,每个 worker 循环取任务直到取完;用 Promise.all 等待所有 worker 结束

all 区别all 管「是否全部完成」;并发池管「同时进行几个」。


六、async/await 工程实践

6.1 定义(与 Promise 的对应)

  1. async 函数返回值一定是 Promisereturn vPromise.resolve(v)throw ePromise.reject(e)
  2. await 等待 Promise 定型;resolved 得值,rejected 等价 throw
  3. async/await 是 Promise 的语法糖await 之后的代码等价于 .then 里的续体。

6.2 串行 vs 并行

const delay = (ms, v) =>
  new Promise((r) => setTimeout(() => r(v), ms));

// 串行 — 总耗时约 300ms
for (const id of [1, 2, 3]) {
  await delay(100, id);
}

// 并行 — 总耗时约 100ms
await Promise.all([1, 2, 3].map((id) => delay(100, id)));

易错:循环里写 async 回调但不 await,会导致并发失控且错误难捕获:

// ❌ forEach 不会等待 async 回调
urls.forEach(async (url) => {
  await fetch(url);
});

// ✅ for...of + await,或 Promise.all + map
for (const url of urls) await fetch(url);
await Promise.all(urls.map((url) => fetch(url)));

6.3 错误处理

const load = async () => {
  try {
    const a = await fetch("/a");
    const b = await fetch("/b");
    return [a, b];
  } catch (e) {
    console.error("请求失败", e);
    throw e;
  }
};

try/catch 可捕获链中 await 的 rejection;等价于 .then().catch()。顶层 async 函数若未 catch,需 .catch() 或 void 调用处处理,避免 Unhandled rejection

6.4 finally

const loadWithLoading = async () => {
  showLoading();
  try {
    return await fetchData();
  } finally {
    hideLoading(); // 无论成败都会执行
  }
};

finally 不接收前序结果,也不改变 fulfilled/rejected(除非 finallythrowreturn Promise)。


七、静态方法速查

API resolve 时机 reject 时机
Promise.resolve(x) 立即 fulfilled
Promise.reject(e) 立即 rejected
Promise.all(arr) 全部 fulfilled 任一 rejected
Promise.race(arr) 最先 fulfilled 最先 rejected
Promise.allSettled(arr) 全部 settle 几乎不 reject
Promise.any(arr) 任一 fulfilled 全部 rejected

八、易混淆点归纳

  1. 「Promise 同步」 = executor 同步 + then/await 续体是微任务,勿理解成「没有异步」。
  2. all vs race:all 要全部成功;race 要最先定型的那一个
  3. all vs any:all 一败俱败;any 一成则成
  4. allSettled vs all:前者要完整报告;后者快速失败
  5. race 超时不会自动取消慢任务;需要 AbortController 等机制。
  6. await 在 for 里默认串行;并行必须 Promise.all
  7. catch 之后若返回普通值,后续 thenfulfilled,错误链已「修复」。

九、思考与练习

1. Promise.all([p1, p2])Promise.race([p1, p2]) 在 p1、p2 都 fulfilled 时,结果分别是什么?

解析:all[v1, v2] 数组(顺序同传入);race先 fulfilled 的那个值(单个值,不是数组)。

2. 下面输出顺序?

console.log("1");
Promise.resolve().then(() => console.log("2"));
async function f() {
  console.log("3");
  await null;
  console.log("4");
}
f();
console.log("5");

解析:1 → 3 → 5 → 2 → 4await null 等价 await Promise.resolve(null),续体进微任务。

3. 三个 CDN 镜像,任一成功即可加载脚本,用哪个 API?

解析:Promise.any

4. 批量删除 100 条记录,需知道每条成功或失败原因,用哪个 API?

解析:Promise.allSettled,再遍历 status / value / reason

5. 并发池 limit = 2、任务 5 个,同一时刻最多几个在执行?

解析:2 个;一个 worker 完成后才会从共享队列取下一个下标。


总结

  • 机制:三态不可逆;then 返回新 Promise值穿透rejected 穿透executor 同步、then/await 微任务
  • 静态方法all 全成则成;race 先到先得;allSettled 全量报告;any 任一成功。
  • 工程模式race + 超时(配合 abort)、retry 循环limitPool 并发池
  • async/await:语法糖;并行用 Promise.alltry/catch/finally 组织错误与清理。

下一篇进入 数据结构基础:栈与队列、链表、二叉树、哈希与 Map/Set,并强调白板复杂度与递归/迭代取舍。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐