Promise 与 async/await
系列文章目录
《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)
- 第 01 篇:数据类型与类型判断
- 第 02 篇:变量声明与作用域
- 第 03 篇:闭包与高阶函数
- 第 04 篇:函数工厂
- 第 05 篇:this 指向与绑定
- 第 06 篇:原型与原型链
- 第 07 篇:类与继承
- 第 08 篇:JS 执行机制与异步队列
- 第 09 篇:数组常用方法
- 第 10 篇:字符串算法
- 第 11 篇:常见手写题合集(上)
- 第 12 篇:常见手写题合集(下)
- 第 13 篇:Promise 与 async/await(本文)
文章目录
前言
第 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
规则摘要:
- 回调返回普通值 → 下一个
then收到该值(被Promise.resolve包装)。 - 回调返回 Promise / thenable → 下一个
then等待其状态再决定 fulfilled/rejected。 - 回调
throw或返回 rejected Promise → 后续 fulfilled 跳过,进入 rejected 链,直到被catch处理。
1.3 值穿透
当 onFulfilled 或 onRejected 不是函数时,值会原样传递到下一个 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 没有异步,而是指:
new Promise(executor)里的executor函数同步执行(resolve/reject调用前的代码都在当前调用栈跑完)。.then/.catch/.finally注册的回调才是微任务,在本轮同步代码结束后、下一个宏任务前执行(见第 08 篇)。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)
);
注意:
- 先 reject 的胜出——若请求在超时后完成,
fetch仍在进行,可能造成浪费;生产环境配合AbortController取消请求。 - 超时 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 的对应)
async函数返回值一定是 Promise;return v→Promise.resolve(v),throw e→Promise.reject(e)。await等待 Promise 定型;resolved 得值,rejected 等价throw。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(除非 finally 内 throw 或 return 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 |
八、易混淆点归纳
- 「Promise 同步」 = executor 同步 + then/await 续体是微任务,勿理解成「没有异步」。
allvsrace:all 要全部成功;race 要最先定型的那一个。allvsany:all 一败俱败;any 一成则成。allSettledvsall:前者要完整报告;后者快速失败。race超时不会自动取消慢任务;需要AbortController等机制。await在 for 里默认串行;并行必须Promise.all。catch之后若返回普通值,后续then走 fulfilled,错误链已「修复」。
九、思考与练习
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 → 4。await 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.all;try/catch/finally组织错误与清理。
下一篇进入 数据结构基础:栈与队列、链表、二叉树、哈希与 Map/Set,并强调白板复杂度与递归/迭代取舍。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)