async/await 深入
async/await 是 Promise 之上的语法糖,让异步代码看起来像同步代码。本文从原理出发,讲透 async 函数的返回值、await 的暂停机制、错误处理、并行与串行执行,最后用一张陷阱清单帮你在实战中避开常见坑。
一、async 函数:定义与返回值
任何函数加上 async 关键字就变成 async 函数,它总是返回一个 Promise,无论函数体里写什么:
async function fn1() {
return 42; // 普通值会被自动包装成 Promise.resolve(42)
}
fn1().then(v => console.log(v)); // 42
const fn2 = async () => "hello"; // 箭头函数写法
fn2().then(console.log); // hello
const obj = {
async getData() { return { ok: true }; } // 对象方法写法
};
obj.getData().then(console.log); // { ok: true }async 函数体内发生同步抛出(throw)时,返回的 Promise 会以 rejected 状态出现,而不是把错误抛出到外面:
async function fail() {
throw new Error("出错了");
}
fail().catch(err => console.log(err.message)); // 出错了| 函数体内 | 返回的 Promise 状态 | 结果值 |
|---|---|---|
return 普通值 | fulfilled(成功) | 该值 |
return 一个 Promise | 被"吸收"(absorb),跟随其状态 | 内层 Promise 的结果 |
| 没有 return | fulfilled(成功) | undefined |
抛出异常 / throw | rejected(失败) | 错误对象 |
关键点:async 函数本身不会阻塞调用方,它立即返回一个 Promise,函数体开始执行,遇到 await 才暂停。
二、await 语法与暂停原理
await 只能在 async 函数内部使用(顶层 await 见第八节)。它的作用是把一个异步值"解包"成普通值,并暂停当前函数,直到该值 settle:
async function getConfig() {
const res = await fetch("/api/config"); // 暂停:等 fetch 完成
const data = await res.json(); // 暂停:等 json() 完成
return data; // 继续:拿到普通值
}2.1 原理:生成器 + Promise 的语法糖
await 的暂停/恢复机制本质上就是**生成器(generator)**的 yield 暂停 + Promise 的 .then 恢复。手写一个简化版 async/await,逻辑一目了然:
function myAsync(genFn) {
const iterator = genFn();
function step(value) {
const { value: result, done } = iterator.next(value);
if (done) return Promise.resolve(result);
return Promise.resolve(result).then(
v => step(v), // 成功:把值传回生成器继续
e => iterator.throw(e), // 失败:在生成器内部抛出
);
}
return Promise.resolve().then(() => step());
}
// 用生成器"模拟" async 函数
const getUser = myAsync(function* () {
const userId = 1;
const user = yield fetch(`/api/users/${userId}`).then(r => r.json());
return user;
});await x 大致等价于 yield x,引擎在拿到 Promise 结果后自动把值塞回函数继续执行——这就是"暂停原理"。
2.2 await 之后的代码就是 .then 回调
async function demo() {
const a = await Promise.resolve(1);
const b = await Promise.resolve(a + 1);
return b;
}
// 等价于:
Promise.resolve(1)
.then(a => Promise.resolve(a + 1))
.then(b => b);三、await 等待的三种值
await 后面可以跟任意值,引擎按类型分三种情况处理:
// 1. Promise:等待其 settle 后取出结果
const p = await Promise.resolve("来自 Promise");
// 2. thenable:任何带 then 方法的对象
const thenable = {
then(resolve) { setTimeout(() => resolve("来自 thenable"), 100); },
};
console.log(await thenable); // 来自 thenable
// 3. 普通值:直接作为结果(等价于 Promise.resolve(value))
const n = await 100; // 100
console.log(n); // 100| await 后面的值 | 处理方式 | 结果 |
|---|---|---|
| Promise 实例 | 等待其 settle | fulfilled 的值或抛出 rejected 的原因 |
| thenable 对象 | 调用它的 then 方法并接入 Promise 机制 | then 中 resolve 的值 |
| 普通值(数字、字符串、对象等) | 包装为已成功 Promise | 原值原样返回 |
四、async/await 与 Promise 链等价转换
两种写法可以互相改写,理解对应关系有助于排查问题:
// Promise 链版本
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => posts.filter(p => p.published))
.catch(err => console.error("失败:", err));
// async/await 版本
async function load() {
try {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
return posts.filter(p => p.published);
} catch (err) {
console.error("失败:", err);
}
}| async/await 语法 | 等价 Promise 语法 |
|---|---|
const v = await p; | p.then(v => ...) |
async 函数中的 return x; | resolve(x) |
try { ... } catch (e) { ... } | .catch(e => ...) |
finally { ... } | .finally(() => ...) |
throw e; | reject(e) |
五、错误处理
5.1 try/catch 捕获单个 await 的失败
async function loadData() {
try {
const data = await fetch("/api/data").then(r => r.json());
return data;
} catch (err) {
console.error("请求失败:", err.message);
return { fallback: true }; // 降级兜底
}
}注意:try 只能捕获块内 await 的错误。如果 fetch 是发出去后才 await,期间抛出的错误依然会被 catch 抓住,因为 await 表达式本身是同步执行的求值过程。
5.2 多个异步步骤:try/catch 一次性包裹
async function pipeline() {
try {
const a = await stepA();
const b = await stepB(a);
const c = await stepC(b);
return c;
} catch (err) {
// 任何一个步骤失败都会走到这里
console.error("管线中断:", err);
}
}5.3 与 Promise.all / Promise.allSettled 配合
Promise.all:任何一个失败就整体失败,适合"全部成功才继续"。Promise.allSettled:等所有都 settle,逐个查看结果,适合"各自成败不影响整体"。
async function fetchAll() {
const results = await Promise.allSettled([
fetch("/api/a"),
fetch("/api/b"),
fetch("/api/c"),
]);
for (const r of results) {
if (r.status === "fulfilled") {
console.log("成功:", r.value);
} else {
console.log("失败:", r.reason.message);
}
}
}| 场景 | 推荐组合 | 错误行为 |
|---|---|---|
| 必须全部成功 | Promise.all + try/catch | 任一失败立即整体 reject |
| 允许部分失败 | Promise.allSettled | 等全部结束,逐项看 status |
| 取最快结果 | Promise.race + try/catch | 谁先 settle 用谁 |
六、并行执行
await 会让代码串行等待,但很多时候异步任务是互相独立的,应该先全部发起、再统一等待:
// 错误示范:串行执行,耗时 = t1 + t2 + t3
async function slow() {
const a = await fetch("/api/a"); // 等它完成
const b = await fetch("/api/b"); // 才开始下一个
const c = await fetch("/api/c");
}
// 正确示范:并行执行,耗时 ≈ max(t1, t2, t3)
async function fast() {
const [a, b, c] = await Promise.all([
fetch("/api/a"),
fetch("/api/b"),
fetch("/api/c"),
]);
}注意:Promise.all 接收的数组里写的是发起调用的表达式,表达式在传入瞬间就已经开始执行,三个请求是同时发出的。
6.1 带并发限制的并行
同时发起上百个请求会压垮服务器,需要限制并发数,例如一次最多跑 3 个:
async function runWithLimit(tasks, limit = 3) {
const results = [];
const queue = [...tasks];
async function worker() {
while (queue.length) {
const task = queue.shift();
results.push(await task());
}
}
await Promise.all(Array.from({ length: limit }, worker));
return results;
}
// 用法:100 个任务,并发 5
const jobs = Array.from({ length: 100 }, (_, i) => () => fetch(`/api/item/${i}`));
await runWithLimit(jobs, 5);七、串行 vs 并行对比
| 对比项 | 串行(逐个 await) | 并行(Promise.all) |
|---|---|---|
| 写法 | for-of + await | Promise.all([...]) |
| 总耗时 | 各任务耗时之和 | 最慢任务耗时 |
| 结果顺序 | 与执行顺序一致 | 按数组下标返回 |
| 依赖关系 | 支持"上一步结果喂下一步" | 任务间必须独立 |
| 失败行为 | 失败处中断,后续不执行 | 任一失败整体 reject |
选择原则:任务之间有依赖用串行,没有依赖就用并行,两者可以混用——外层并行,内层串行。
八、顶层 await(ES2022)
从 ES2022 起,模块(ES Module)的顶层可以直接使用 await,不必包裹在 async 函数里,前提是文件是 .mjs、type: "module" 或浏览器 <script type="module">:
// config.mjs —— 模块顶层直接 await
const config = await fetch("/api/config").then(r => r.json());
export default config;
// main.mjs —— 依赖 config.mjs 的模块会等它加载完成
import config from "./config.mjs";
console.log(config);特点与注意点:
- 顶层 await 会阻塞整个模块树的加载,依赖它的模块必须等待。
- 适合:启动时加载配置、动态导入资源、初始化连接。
- 不适合:对加载时间敏感的首屏逻辑。
- 在 CommonJS(
require)或普通<script>中不适用,会报语法错误。
九、循环中的 await
9.1 for 循环 + await(串行)
async function serial() {
for (let i = 0; i < 3; i++) {
const data = await fetch(`/api/page/${i}`);
console.log(`第 ${i} 页加载完成`);
}
}9.2 for-of + await
for...of 与 await 是天然搭档,逐个处理数组元素且保持顺序:
const ids = [1, 2, 3];
async function serialById() {
for (const id of ids) {
const user = await fetchUser(id);
save(user); // 逐个保存
}
}9.3 陷阱:forEach 不等待 async 回调
forEach 不会等待回调里的 await,数组遍历立即结束:
async function broken() {
ids.forEach(async (id) => {
const user = await fetchUser(id); // 这里的 await 只暂停回调自身
save(user);
});
console.log("遍历已经结束"); // 先打印!保存操作还没完成
}| 循环写法 | 是否等待 await | 执行顺序 |
|---|---|---|
for + await | 等待 | 严格串行 |
for...of + await | 等待 | 严格串行 |
forEach + async 回调 | 不等待 | 同时发起 |
map + async 回调 | 不等待(但返回 Promise 数组) | 同时发起 |
for await...of(异步迭代器) | 等待 | 串行消费异步数据流 |
十、await 与微任务
await 之后的代码会被调度为微任务执行,遵循"微任务先于宏任务"的规则。看一个执行顺序题:
console.log("1: 同步开始");
async function demo() {
console.log("2: async 内同步代码");
await Promise.resolve();
console.log("4: await 之后(微任务)");
}
demo();
setTimeout(() => console.log("5: 定时器(宏任务)"), 0);
console.log("3: 同步结束");
// 输出顺序:1 → 2 → 3 → 4 → 5原理:await 相当于把后续代码放进 Promise.then 回调,而 .then 回调属于微任务,会在当前宏任务收尾前执行,但一定晚于所有同步代码。
十一、async 函数中的 return 值
return普通值 → Promise 以该值 fulfilled。return一个 Promise → 外层 Promise 吸收它,跟随其最终结果:
async function outer() {
return Promise.resolve("内层结果");
}
outer().then(console.log); // 内层结果(吸收,不是嵌套 Promise)
async function outer2() {
return Promise.reject(new Error("内层失败"));
}
outer2().catch(e => console.log(e.message)); // 内层失败(rejected 也会传递)- 不加
return→ fulfilled 且值为undefined。 return await p与return p在结果上等价(吸收),但return await在 try/catch 场景有意义——它能让错误在当前函数的 catch 中处理:
async function withReturnAwait() {
try {
return await risky(); // 错误在这里被 catch 抓住
} catch (err) {
console.log("被本函数捕获:", err.message);
}
}十二、常见陷阱
12.1 忘记 await
async function main() {
const user = fetchUser(1); // 忘了 await!拿到的是 Promise 对象
console.log(user.id); // undefined
const real = await fetchUser(1); // 正确
}12.2 竞态条件:多个请求响应的先后不可控
async function raceBug() {
const fast = fetch("/api/fast"); // 先发起
const slow = fetch("/api/slow"); // 后发起
const slowData = await slow; // 先等慢的
const fastData = await fast; // 再等快的
// 若两个请求都更新同一个全局状态,响应顺序可能造成脏数据
}修复思路:不要依赖发起顺序,用 Promise.all 一次性取齐,或用请求 ID/时间戳校验响应是否"过期"。
12.3 串行化过慢
把本可并行的请求写成逐个 await,是性能问题的头号来源。判断标准:后面的请求是否依赖前面的结果。不依赖就改成 Promise.all。
12.4 异步回调里的错误被吞
// 错误示范:forEach 里抛错不会被外层 catch 捕获
async function bad() {
try {
[1, 2, 3].forEach(async (id) => {
const r = await fetch(`/api/${id}`);
if (!r.ok) throw new Error("失败"); // 这个错误丢失了!
});
} catch (err) {
console.error(err); // 永远执行不到
}
}12.5 陷阱速查表
| 陷阱 | 现象 | 修复 |
|---|---|---|
| 忘记 await | 拿到 Promise 而非值 | 检查赋值处是否遗漏 await |
| forEach 内 await | 回调不被等待 | 改用 for-of + await |
| 错误在回调中抛出 | 外层 try/catch 抓不到 | 用 Promise.all 包裹或改为 async 流程 |
| 串行 await 独立请求 | 总耗时叠加 | 改为 Promise.all |
| 依赖返回顺序 | 数据被覆盖 | 校验响应有效性或用 all 取齐 |
| await 普通对象 | 值被原样返回 | 属正常行为,但别误以为会等待 setTimeout 等非 Promise 异步 |
判断 await 会不会真的等待,只看它后面是不是 Promise 或 thenable——await setTimeout(...) 不会等待定时器,因为 setTimeout 返回的是定时器 id(或 Timeout 对象),不是 Promise。
async/await 本质是"用同步的写法表达异步的流程",把上面这张陷阱表记牢,就能在并发、错误、顺序三方面写出健壮的异步代码。