现代异步 async/await
用同步的写法,处理异步的事
🎯 学习目标
学完本章,你将能够:
- 能够使用 async/await 重写 Promise 链式代码,提升可读性
- 能够使用 try/catch 捕获 async 函数中的错误
- 能够区分串行和并行执行,合理使用 Promise.all 优化性能
- 能够使用 for await...of 遍历异步可迭代对象
- 能够实现简单的并发控制,限制同时执行的异步任务数量
对比一下
两种写法,同一个功能,哪个更易读?
// Promise 链式写法
function getUser(id) {
return fetch(`/api/users/${id}`)
.then(res => res.json())
.then(user => fetch(`/api/orders/${user.id}`))
.then(res => res.json())
.catch(err => console.error(err));
}
// async/await 写法
async function getUser(id) {
try {
let res = await fetch(`/api/users/${id}`);
let user = await res.json();
let orders = await fetch(`/api/orders/${user.id}`);
return await orders.json();
} catch (err) {
console.error(err);
}
}
第二种写法读起来就像同步代码——一行一行往下走,逻辑清晰。这就是 async/await 的魅力。
它不是什么新魔法,本质上是 Promise 的语法糖。但这个糖实在太甜了。
Part 1: async 函数
加个关键字,普通函数变成 Promise 工厂
对比开场的两种写法,async/await 版本读起来就像同步代码——没有 .then(),没有嵌套。秘密就在 async 关键字上:把它加在函数声明前面,函数就自动返回 Promise。这一部分先搞懂 async 函数的基本行为,为后面的 await 打好基础。
async 函数
加个关键字,普通函数变成 Promise 工厂
async 函数是使用 async 关键字声明的函数。它总是返回一个 Promise。即使你返回一个普通值,也会被自动包装成 Promise.resolve(value)。
说人话:async 就像给函数贴了个标签,告诉 JS 引擎"这个函数里面有异步操作,请用 Promise 的方式对待它"。
这个比喻只是便于记忆。实际上 async 做的事情就是改写函数的返回值处理逻辑,在引擎层面并没有什么"标签"。
async function greet() {
return "Hello!"; // 等同于 return Promise.resolve("Hello!")
}
greet().then(msg => console.log(msg)); // "Hello!"
// async 箭头函数
const greet2 = async () => "Hello!";
// async 方法
const obj = {
async fetchData() {
return { data: "ok" };
}
};
// 抛出错误 → rejected Promise
async function fail() {
throw new Error("出错了");
}
fail().catch(err => console.log(err.message)); // "出错了"
Part 2: await
暂停函数,等 Promise 完成再继续
async 函数本身只是返回 Promise,真正让代码"看起来同步"的是 await。它会在 Promise 完成前暂停当前函数,拿到结果后再继续往下走。但注意:暂停的不是整个程序,只是这一个函数,外部代码照常运行。理解了"暂停不等于阻塞",才算真正掌握了 await。
await 暂停
等一等,结果来了再继续
await 只能在 async 函数中使用(ES2022 顶层 await 除外)。它会暂停当前函数的执行,等待 Promise 完成,然后返回结果。
async function fetchUser() {
let response = await fetch("/api/users/1");
let user = await response.json();
console.log(user);
return user;
}
// await 后面不一定是 Promise
async function example() {
let value = await 42; // 等同于 await Promise.resolve(42)
let name = await "hello";
console.log(value, name);
}
await 流程
暂停 ≠ 阻塞

图:await 执行流程——async 函数的暂停与恢复机制
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一个纵向流程图,展示 async 函数从开始到结束的完整执行过程,包含 7 个节点 - 各元素关系:从上到下依次排列——"async 函数开始执行"→"遇到 await"(橙色高亮),在 await 节点处左右分栏并行展示两条路径:左侧展示 async 函数内部的暂停/恢复,右侧展示主线程继续执行。左侧路径"暂停当前函数"→"函数返回 pending Promise"(蓝色),右侧路径"主线程继续"→"执行其他同步代码"→"事件循环正常运行"→"Promise 完成"→"恢复 async 函数"(绿色)→"await 返回结果"→"继续执行下一行" - 关键标注:在"遇到 await"节点旁标注"暂停 ≠ 阻塞",用虚线框将主线程路径和 async 函数路径分开,箭头用不同颜色区分暂停路径(蓝色)和恢复路径(绿色) - 配色建议:浅蓝/浅灰背景,深色文字,"遇到 await"节点用橙色标注,暂停路径用蓝色箭头,恢复路径用绿色箭头,主线程路径用灰色箭头 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
关键:await 只暂停 async 函数内部,不会阻塞整个线程。外部代码照常运行。
IIFE 包装
没有 async 函数?自己造一个
// 在不支持顶层 await 的环境中,用 IIFE 包裹
(async () => {
let data = await fetchData();
console.log(data);
})();
IIFE(Immediately Invoked Function Expression)就是"立即执行函数表达式"。定义完立刻调用,不需要名字。
Part 3: 错误处理
异步出错怎么办?四种模式各有适用场景
await 拿到的是 Promise 的结果,但如果 Promise 被拒绝了呢?不处理就直接崩溃。同步代码用 try/catch,异步也一样——但还有更灵活的模式:单独捕获每个 await,或者用包装函数避免到处写 try/catch。这一部分给出四种错误处理方式,帮你根据场景选择最合适的。
try/catch
最直观的错误处理方式
async function getUser(id) {
try {
let response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
let user = await response.json();
return user;
} catch (error) {
console.error("获取用户失败:", error.message);
return null;
} finally {
console.log("请求完成");
}
}
模式一:try/catch 包裹整个函数体。适合简单场景,所有错误统一处理。
错误处理模式
四种方式,各有适用场景
// 模式一:try/catch 包裹全部
async function mode1() {
try {
let a = await step1();
let b = await step2(a);
return b;
} catch (e) {
console.error("出错了:", e);
}
}
// 模式二:每个 await 单独 try/catch
async function mode2() {
let a = await step1().catch(e => {
console.error("step1 失败:", e);
return defaultValue;
});
let b = await step2(a).catch(e => {
console.error("step2 失败:", e);
return defaultValue;
});
return b;
}
// 模式三:包装函数(不写 try/catch)
async function mode3() {
let [a, err1] = await safeExecute(step1());
if (err1) return handleError(err1);
let [b, err2] = await safeExecute(step2(a));
if (err2) return handleError(err2);
return b;
}
async function safeExecute(promise) {
try { return [await promise, null]; }
catch (e) { return [null, e]; }
}
// 模式四:catch 链式
async function mode4() {
let a = await step1().catch(handleError);
let b = await step2(a).catch(handleError);
return b;
}
Part 4: 并行优化
没有依赖关系的操作,别排队,一起跑
连续写两个 await,它们是串行执行的——第一个完成才开始第二个。但如果两个请求互不依赖,串行等于白等一倍时间。用 Promise.all 配合 await,可以让多个异步操作并行跑。这一部分讲清楚串行和并行的区别,以及如何控制并发数量防止"一次性发太多请求"。
串行与并行
同样的操作,时间差一半
// 串行(慢)——一个接一个
async function serial() {
const a = await fetchA(); // 等待 1s
const b = await fetchB(); // 再等 1s,总共 2s
return [a, b];
}
// 并行(快)——同时发起
async function parallel() {
const [a, b] = await Promise.all([
fetchA(), // 同时发起
fetchB() // 同时发起,总共 1s
]);
return [a, b];
}
时间线对比
可视化串行与并行的执行差异

图:串行与并行时间线对比——2 秒 vs 1 秒
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:左右并排的两条水平时间线对比图。上方为"串行执行"时间线,下方为"并行执行"时间线,每条时间线左侧有标题标签,右侧有总耗时标注 - 各元素关系:上方串行时间线——从左到右依次排列"fetchA(1s)"红色色块和"fetchB(1s)"红色色块,总长 2s,箭头串联;下方并行时间线——"fetchA(1s)"绿色色块和"fetchB(1s)"绿色色块上下堆叠对齐,同时开始同时结束,总长 1s;用虚线在 1s 处画竖线对齐两条时间线 - 关键标注:串行时间线右上角标注"总共 2s",并行时间线右上角标注"总共 1s",1s 处的虚线旁标注"快一倍",串行色块用红色,并行色块用绿色,形成视觉对比 - 配色建议:浅蓝/浅灰背景,深色文字,串行色块用浅红色(#d0021b),并行色块用浅绿色(#7ed321),关键箭头用橙色标注,时间轴刻度用灰色细线 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
// 按需并行
async function optimized() {
// 不互相依赖的请求并行
let [users, config] = await Promise.all([
fetchUsers(),
fetchConfig()
]);
// 依赖前面的结果
let details = await fetchDetails(users[0].id);
return { users, config, details };
}
批量并发
控制并发数量,既快又稳
async function batchProcess(items, concurrency = 5) {
let results = [];
for (let i = 0; i < items.length; i += concurrency) {
let batch = items.slice(i, i + concurrency);
let batchResults = await Promise.all(
batch.map(item => processItem(item))
);
results.push(...batchResults);
}
return results;
}
每批发 5 个请求,等这 5 个全部完成后再发下一批。避免一次性发 1000 个请求导致服务器崩溃。
Part 5: 顶层 await
模块顶层直接 await,不用再包一层
以前用 await 必须包在 async 函数里,顶部代码想 await 就得写一个 IIFE。ES2022 的顶层 await 终于免去了这个麻烦:在 ES 模块顶层直接 await,模块会等它完成后再暴露给导入方。适合配置加载、条件导入等场景。
顶层 await
模块级别直接等待
// config.mjs
const config = await loadConfig();
export default config;
// app.mjs
import config from './config.mjs';
// 等 config 加载完成后才执行后续代码
console.log(config);
// 条件加载
const adapter = await (isNode
? import('./node-adapter.js')
: import('./browser-adapter.js'));
.mjs 文件或在 package.json 中设置 "type": "module")。CommonJS 模块不支持。
Part 6: 异步迭代
逐个消费异步数据流
分页接口每次返回一页数据,想拿全部数据怎么办?for await...of 配合异步生成器,可以像遍历普通数组一样遍历异步数据源。每拿一页处理一页,不需要一次性加载全部,内存占用恒定。在文件逐行读取、流式处理等场景中非常好用。
for await
逐个消费异步数据流
// 异步生成器
async function* fetchPages(url) {
let page = 1;
while (true) {
let res = await fetch(`${url}?page=${page}`);
let data = await res.json();
if (data.items.length === 0) return;
yield data.items;
page++;
}
}
// 用 for await...of 消费
async function getAllItems() {
let all = [];
for await (let items of fetchPages("/api/items")) {
all.push(...items);
}
return all;
}
// 逐行读取文件(Node.js)
const fs = require('fs/promises');
const readline = require('readline');
async function readFileLines(path) {
const stream = fs.createReadStream(path);
const rl = readline.createInterface({ input: stream });
for await (let line of rl) {
console.log(line);
}
}
常见 async 错误
问题一:忘记 await
// 错误:拿到的是 Promise,不是结果
async function wrong() {
let user = fetchUser(1); // 忘记 await
console.log(user); // Promise { <pending> }
}
// 正确
async function right() {
let user = await fetchUser(1);
console.log(user); // { name: "张三", ... }
}
问题二:循环中的串行陷阱
// 慢:串行执行(每个请求等上一个完成)
for (let url of urls) {
let data = await fetch(url);
console.log(data);
}
// 快:并行执行
let promises = urls.map(url => fetch(url));
let results = await Promise.all(promises);
并发控制
限制同时运行的 Promise 数量
async function limitConcurrency(tasks, limit) {
const results = [];
const executing = new Set();
for (const task of tasks) {
// 如果正在执行的数量达到上限,等待任意一个完成
if (executing.size >= limit) {
await Promise.race(executing);
}
// 创建 Promise 并加入执行集合
const p = Promise.resolve().then(() => task());
const wrapped = p.then(result => {
executing.delete(wrapped);
return result;
});
executing.add(wrapped);
results.push(wrapped);
}
// 等待所有剩余任务完成
return Promise.all(results);
}
// 使用场景:批量请求不压垮服务器
const urls = Array.from({ length: 100 }, (_, i) =>
`https://api.example.com/items/${i}`
);
async function fetchAll() {
const tasks = urls.map(url => () => fetch(url));
// 最多同时 5 个请求
const responses = await limitConcurrency(tasks, 5);
const data = await Promise.all(responses.map(r => r.json()));
console.log(`获取了 ${data.length} 条数据`);
}
Promise.race 只能告诉我们"有一个完成了",但不会返回它在数组中的位置。用 Set 管理 executing 并在完成时 delete,才能正确追踪剩余并发数。
异步迭代实战
用异步生成器优雅处理分页
// 异步生成器:自动翻页,逐页 yield 数据
async function* paginate(url, transform) {
let page = 1;
let hasMore = true;
while (hasMore) {
const res = await fetch(`${url}?page=${page}`);
const data = await res.json();
if (data.items.length === 0) {
hasMore = false;
return;
}
// 用 transform 函数处理每一页数据
yield transform ? transform(data.items) : data.items;
page++;
}
}
// 消费数据流:逐页处理,内存友好
async function syncAllUsers() {
const batchSize = 50;
let total = 0;
for await (let users of paginate('/api/users', items =>
items.filter(u => u.active).map(u => ({
id: u.id,
name: u.name
}))
)) {
await batchSave(users);
total += users.length;
console.log(`已同步 ${total} 条用户`);
}
console.log(`同步完成,共 ${total} 条`);
}
// 模拟批量保存
async function batchSave(users) {
await new Promise(r => setTimeout(r, 100)); // 模拟 IO
}
异步生成器的优势:不需要一次性加载所有数据。每处理完一页才请求下一页,内存占用恒定。配合 transform 函数可以在 yield 前对数据做清洗、过滤、映射。
Part 7: 进阶实战
把 async/await、错误处理、并发控制组合起来
前面的知识都学完了,现在要把它们组合起来解决真实问题:用 async/await 重写 Promise 链、给请求加超时控制、实现一个带并发限制的批量处理器。三道练习题从易到难,做完就算真正出师了。
练习一
任务:用 async/await 重写下面的 Promise 链。
function getData() {
return fetch("/api/data")
.then(res => res.json())
.then(data => {
console.log(data);
return data;
})
.catch(err => console.error(err));
}
// 你的 async/await 版本:
参考答案
async function getData() {
try {
let res = await fetch("/api/data");
let data = await res.json();
console.log(data);
return data;
} catch (err) {
console.error(err);
}
}
练习二
任务:实现一个 withTimeout(fetch(url), 3000) 函数,如果请求 3 秒内没完成就抛出超时错误。
// 提示:Promise.race + setTimeout
// 你的代码:
参考答案
async function withTimeout(promise, ms) {
let timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("超时")), ms)
);
return Promise.race([promise, timeout]);
}
练习三
任务:实现 promiseMap(items, fn, concurrency)——对数组中的每个元素执行异步函数 fn,但限制并发数量为 concurrency。返回所有结果的数组(保持原顺序)。
// 提示:维护一个执行中的 Promise 集合
// 每当有空位就启动下一个任务
// 最后 Promise.all 保证顺序
// 你的代码:
参考答案
async function promiseMap(items, fn, concurrency) {
const results = new Array(items.length);
let nextIndex = 0;
async function worker() {
while (nextIndex < items.length) {
const index = nextIndex++;
results[index] = await fn(items[index], index);
}
}
// 启动 concurrency 个 worker 并行执行
const workers = Array.from(
{ length: Math.min(concurrency, items.length) },
() => worker()
);
await Promise.all(workers);
return results;
}
// 测试
async function test() {
const items = [1, 2, 3, 4, 5, 6, 7, 8];
const results = await promiseMap(
items,
async (n) => {
await new Promise(r => setTimeout(r, 100));
return n * 2;
},
3 // 最多 3 个并发
);
console.log(results); // [2, 4, 6, 8, 10, 12, 14, 16]
}
test();
📝 要点总结
本章核心知识点
async 函数
声明异步函数,自动返回 Promise
await 暂停
等待 Promise resolve 后继续执行
错误处理
try/catch 捕获异步错误
串行与并行
await 串行,Promise.all 并行
并发控制
限制同时执行的异步任务数量
下一章
async/await 让异步代码变得优雅。下一章我们探索一个更高级的特性:生成器与迭代器——能暂停、能恢复的函数。