第4章 4.3 async/await 异步函数

🎯 开场:为什么你需要一个"更爽"的异步写法?

上一章我们学会了 Promise,用 .then() 把异步任务串起来执行。你可能已经感觉到了——当业务变复杂时,promise.then().then().then() 会变成一长串嵌套,看起来眼花,读起来费劲。

举个例子,你想实现这个逻辑:

  1. 先读取用户信息
  2. 再根据用户 ID 获取他的订单列表
  3. 最后把所有订单的发货状态查出来

用 Promise 写,大概是这样:

fetchUser()
.then(user => fetchOrders(user.id))
.then(orders => Promise.all(orders.map(order => fetchShippingStatus(order.id))))
.then(results => console.log(results))
.catch(err => console.error(err));

能跑,但括号\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n在哪、谁返回谁,一眼扫过去有点晕。

学完这一章,你能:
- 用 async/await 把上面的代码改写成几乎像"同步代码"的样子
- 优雅地处理错误,不再担心 catch 漏接
- 并行执行多个异步任务,不卡住主线程


🧱 基础:async/await 到底是什么?

4.3.1 先记住两个规则(生活版)

想象你去火锅店点菜:

  • 服务员 = async 函数,你对她喊一声"来个鸳鸯锅",她就去后厨忙了,不会堵在门口等你吃完
  • 等菜上桌 = await,你坐在位置上干等着,直到菜端上来才动筷子

async/await 就是这个道理:让异步代码看起来像同步的,读起来更顺口

规则一async 函数会自动返回 Promise,不用你手动 new Promise()

规则二await 只能在 async 函数里用,意思是"等这个 Promise 结果出来再往下走"

4.3.2 第一个 async 函数(抄改就能跑)

先来一个最简单的,感受一下:

// 这是一个 async 函数
async function sayHello() {
return "你好,我是 async 函数";
}

// 调用它
sayHello().then(result => {
console.log(result);  // 输出:你好,我是 async 函数
});

解释:加了 async 的函数会自动返回一个 Promise,所以你能用 .then() 接住它的返回值。

等价于你上一章写的:

function sayHello() {
return new Promise(resolve => resolve("你好,我是 async 函数"));
}

看到了吗?async 帮你省掉了 new Promise() 这一步。

4.3.3 真正用 await 等待结果

光有 async 不够,得配合 await 才能"等":

// 模拟一个 1 秒后返回结果的函数
function waitAndReturn(msg, delay = 1000) {
return new Promise(resolve => {
setTimeout(() => resolve(msg), delay);
});
}

// 用 async/await 来调用
async function main() {
console.log("开始执行...");

const result = await waitAndReturn("等到了!", 1000);

console.log(result);  // 1 秒后输出:等到了!
console.log("执行完毕");
}

main();

预期输出(等 1 秒后一起出来):

开始执行...
等到了!
执行完毕

解释await waitAndReturn(...) 会暂停 main() 函数的执行,等 waitAndReturn 的 Promise resolved 才继续往下走。这就像你点了菜,必须等菜上了才吃下一口。

4.3.4 把上一章的 Promise 链改成 await 写法

来兑现开头的承诺——把那段嵌套 .then() 改成清爽的 await 风格:

// 假设这些函数都返回 Promise
function fetchUser() {
return new Promise(resolve => setTimeout(() => resolve({ id: 1, name: "小明" }), 500));
}

function fetchOrders(userId) {
return new Promise(resolve => setTimeout(() => resolve(["订单A", "订单B"]), 500));
}

function fetchShippingStatus(order) {
return new Promise(resolve => setTimeout(() => resolve(`${order} 已发货`), 300));
}

// 用 async/await 重写
async function getOrderStatuses() {
try {
const user = await fetchUser();
console.log("获取到用户:", user.name);

const orders = await fetchOrders(user.id);
console.log("获取到订单:", orders);

// 重点:并行获取所有订单状态,不用一个个 await
const statuses = await Promise.all(
  orders.map(order => fetchShippingStatus(order))
);

console.log("所有状态:", statuses);
} catch (error) {
console.error("出错了:", error);
}
}

getOrderStatuses();

预期输出(约 1 秒后):

获取到用户: 小明
获取到订单: ['订单A', '订单B']
所有状态: ['订单A 已发货', '订单B 已发货']

关键点
- await 后面跟普通的 Promise,不用改它的结构
- try/catch 取代了 .catch(),错误处理更直观
- Promise.all() 依然是并行执行多个 Promise 的最佳方式

4.3.5 错误处理:try/catch 才是你的好朋友

上一章我们用 .catch() 处理错误,async 函数里直接用 try/catch

async function riskyTask() {
try {
const data = await mightFailFunction();
console.log("成功:", data);
} catch (error) {
console.error("捕获到错误:", error.message);
} finally {
console.log("无论成功失败都会执行这里");
}
}

解释
- try 块里放可能出错的 await 代码
- catch 捕获错误(类似 .catch()
- finally 不管成功失败都执行(适合做清理工作)


🔥 实战:3 个递进小项目

📦 项目 1(5 分钟):批量获取天气数据

场景:你想查北京、上海、广州三地的天气,每地 API 请求需要 1 秒。

学完你会的:用 Promise.all + async/await 并行请求,不卡主线程。

// 模拟获取天气 API(1 秒后返回)
function fetchWeather(city) {
return new Promise(resolve => {
setTimeout(() => resolve({ city, temp: Math.floor(Math.random() * 20 + 10) + "°C" }), 1000);
});
}

async function getThreeCitiesWeather() {
console.log("开始查询三地天气...");

// 并行请求三地天气,同时开始,同时结束
const results = await Promise.all([
fetchWeather("北京"),
fetchWeather("上海"),
fetchWeather("广州")
]);

console.log("查询完成!");
results.forEach(r => console.log(`${r.city}: ${r.temp}`));
}

getThreeCitiesWeather();

预期输出(约 1 秒后):

开始查询三地天气...
查询完成!
北京: 18°C
上海: 25°C
广州: 22°C

一句话解释:三地请求同时发出,总耗时还是 1 秒,不是 3 秒。


📦 项目 2(15 分钟):读取 CSV 数据并标注状态

场景:你有一个 CSV 文件,里面是订单数据,需要对每条订单查询发货状态,并输出一个新 CSV。

学完你会的:读取 JSON/数组 → 并行异步处理 → 汇总结果。

// 模拟订单数据(实际项目中可能从 CSV 或 API 获取)
const orders = [
{ id: "A001", product: "手机", amount: 2999 },
{ id: "A002", product: "耳机", amount: 199 },
{ id: "A003", product: "键盘", amount: 399 },
{ id: "A004", product: "鼠标", amount: 99 },
];

// 模拟查询发货状态(随机成功/失败,1 秒延迟)
function checkShipping(orderId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
  // 模拟 90% 成功率
  if (Math.random() > 0.1) {
    resolve({ orderId, status: "已发货", expressNo: "SF" + Math.floor(Math.random() * 100000) });
  } else {
    reject(new Error(`订单 ${orderId} 查询失败`));
  }
}, 800);
});
}

async function processOrders() {
console.log(`开始处理 ${orders.length} 个订单...\n`);

const results = [];

for (const order of orders) {
try {
  const shippingInfo = await checkShipping(order.id);
  results.push({
    ...order,
    shippingStatus: shippingInfo.status,
    expressNo: shippingInfo.expressNo
  });
  console.log(`✓ 订单 ${order.id} 已处理`);
} catch (error) {
  console.log(`✗ 订单 ${order.id} 处理失败: ${error.message}`);
  results.push({
    ...order,
    shippingStatus: "处理失败",
    expressNo: "-"
  });
}
}

console.log("\n=== 处理完成 ===");
console.log(JSON.stringify(results, null, 2));
}

processOrders();

预期输出(约 4 秒后,各订单依次处理):

开始处理 4 个订单...

✓ 订单 A001 已处理
✓ 订单 A002 已处理
✗ 订单 A003 处理失败: 订单 A003 查询失败
✓ 订单 A004 已处理

=== 处理完成 ===
[
{
"id": "A001",
"product": "手机",
"amount": 2999,
"shippingStatus": "已发货",
"expressNo": "SF38291"
},
...
]

一句话解释:用 for...of 循环配合 await 逐个处理订单,每个失败不影响其他。


📦 项目 3(15 分钟):做一个「待办任务管理器」

场景:做一个命令行工具,可以添加任务、标记完成、自动保存到本地 JSON 文件。

学完你会的:async/await + 文件读写 + 用户交互组合拳。

const fs = require('fs').promises;  // Node.js 的 Promise 风格文件操作

const TASK_FILE = 'tasks.json';

// 初始化文件(如果不存在)
async function initFile() {
try {
await fs.access(TASK_FILE);
} catch {
await fs.writeFile(TASK_FILE, JSON.stringify([]));
}
}

// 读取所有任务
async function loadTasks() {
const data = await fs.readFile(TASK_FILE, 'utf8');
return JSON.parse(data);
}

// 保存任务到文件
async function saveTasks(tasks) {
await fs.writeFile(TASK_FILE, JSON.stringify(tasks, null, 2));
console.log("✓ 任务已保存\n");
}

// 添加任务
async function addTask(title) {
const tasks = await loadTasks();
const newTask = {
id: Date.now(),
title,
completed: false,
createdAt: new Date().toISOString()
};
tasks.push(newTask);
await saveTasks(tasks);
console.log(`✓ 添加任务: "${title}"`);
}

// 完成任务
async function completeTask(id) {
const tasks = await loadTasks();
const task = tasks.find(t => t.id === id);
if (!task) {
console.log(`✗ 找不到 ID 为 ${id} 的任务`);
return;
}
task.completed = true;
task.completedAt = new Date().toISOString();
await saveTasks(tasks);
console.log(`✓ 完成任务: "${task.title}"`);
}

// 列出所有任务
async function listTasks() {
const tasks = await loadTasks();
console.log("\n=== 待办列表 ===");
if (tasks.length === 0) {
console.log("没有任务,快去添加一个吧!");
} else {
tasks.forEach(t => {
  const status = t.completed ? "✓" : "○";
  const title = t.completed ? `[完成] ${t.title}` : t.title;
  console.log(`${status} [${t.id}] ${title}`);
});
}
console.log();
}

// 主程序
async function main() {
await initFile();

// 演示:添加几个任务
await addTask("学习 async/await");
await addTask("完成作业");
await addTask("给妈妈打电话");

// 列出所有
await listTasks();

// 完成任务
await completeTask(Date.now() - 2000);  // 假设这是第一个任务的 ID
await listTasks();
}

main().catch(console.error);

预期输出

✓ 任务已保存

✓ 添加任务: "学习 async/await"

=== 待办列表 ===
○ [1719123456000] 学习 async/await
○ [1719123458000] 完成作业
○ [1719123459000] 给妈妈打电话

✓ 完成任务: "学习 async/await"

=== 待办列表 ===
○ [1719123458000] 完成作业
○ [1719123459000] 给妈妈打电话
✓ [1719123456000] [完成] 学习 async/await

一句话解释:所有文件操作都用了 await,代码读起来像同步的,但执行起来不阻塞。


💪 进阶:常见坑 + 性能小贴士

坑 1:忘了 await,函数返回的是 Promise 对象,不是值

// ❌ 错误示例
async function getData() {
const result = fetch("/api/data");  // 忘了他是 Promise!
return result;
}

getData().then(console.log);  // 输出 { <pending> } 或 Promise 对象,不是实际数据

// ✅ 正确示例
async function getData() {
const result = await fetch("/api/data");  // 加上 await
return result;
}

解释:没有 await,代码会立即执行并返回 Promise 对象本身,而不是 Promise resolved 后的值。

坑 2:在循环里一个个 await,应该并行时没并行

// ❌ 错误示例:10 个任务串行执行,要等 10 秒
async function badWay() {
for (const url of urls) {
const data = await fetch(url);
console.log(data);
}
}

// ✅ 正确示例:10 个任务并行执行,约 1 秒搞定
async function goodWay() {
await Promise.all(urls.map(url => fetch(url).then(r => r.json()).then(console.log)));
}

原则:独立的任务用 Promise.all 并行,有依赖关系的才用 await 串行。

坑 3:async 函数不写 await,那这个函数毫无意义

// ❌ 错误示例
async function fetchAndProcess() {
fetchData();      // 没 await
processData();    // 这个可能依赖 fetchData 的结果,但不会等
}

// ✅ 正确示例
async function fetchAndProcess() {
const data = await fetchData();
await processData(data);
}

坑 4:Promise.all 一个失败全部失败,用 allSettled 更安全

// ❌ 风险示例:一个失败,全部白做
async function risky() {
const results = await Promise.all([
fetchUser(),
fetchOrders(),
fetchRecommendations()
]);
}

// ✅ 稳健示例:无论成败都收集结果
async function safe() {
const results = await Promise.allSettled([
fetchUser(),
fetchOrders(),
fetchRecommendations()
]);

results.forEach((result, i) => {
if (result.status === "fulfilled") {
  console.log(`任务${i}成功:`, result.value);
} else {
  console.log(`任务${i}失败:`, result.reason);
}
});
}

坑 5:async/await 和 .then() 混用,导致代码风格混乱

保持一致,要么全用 await,要么全用 .then(),不要混在一起。

性能小贴士:善用 Promise.all 减少等待时间

前面项目已经演示了,并行请求多个无关数据时,Promise.all 能让总耗时等于最慢的那个,而不是累加。

调试技巧:async 函数里加日志

async function debugDemo() {
console.log("1. 开始");
const result = await someAsyncTask();
console.log("2. 拿到结果:", result);
const processed = process(result);
console.log("3. 处理完成:", processed);
return processed;
}

好处:async 函数里 console.log 的顺序就是实际执行顺序,方便定位卡在哪一步。


✏️ 练习题

练习 1(2 分钟):改改延迟时间
- 输入:把 waitAndReturn 函数的延迟改成 2000 毫秒
- 预期输出:2 秒后才显示结果
- 提示:只改一个数字

练习 2(3 分钟):加个条件判断
- 输入:在项目 1 的天气查询里,只有温度低于 20°C 才输出 "记得穿外套"
- 预期输出:根据实际随机温度决定是否输出提示
- 提示:用 if (parseInt(r.temp) < 20) 判断

练习 3(5 分钟):处理新数据
- 输入:用项目 2 的方法处理这组数据:[{id: "B001", name: "电脑"}, {id: "B002", name: "平板"}]
- 预期输出:每条数据都标注处理状态
- 提示:复用 processOrders 的逻辑,改一下数据源

练习 4(8 分钟):串起两个项目
- 输入:结合项目 2 和项目 3,先处理订单,再把所有处理结果存到文件
- 预期输出:tasks.json 里包含订单处理结果
- 提示:把项目 2 的 results 传给项目 3 的 saveTasks

练习 5(10 分钟):分析报错
- 输入:下面代码执行后报错 "Cannot read property 'name' of undefined",找出原因并修复

async function broken() {
const user = await fetchUser();
const data = await fetchDetails(user.id);
console.log(data.address.name);  // 报错行
}
  • 预期输出:不报错,输出 address.name 或提示"地址不存在"
  • 提示:可能是 fetchDetails 返回的 data 没有 address 字段,加个判断

📚 作业:做一个「批量图片下载器」

需求描述:做一个工具,输入一批图片 URL,批量下载并保存到本地文件夹。

功能点
1. 支持从 JSON 数组读取 URL 列表
2. 并行下载图片(用 Promise.all
3. 下载完成后统计成功/失败数量

加分项
1. 显示下载进度
2. 失败重试机制(最多重试 3 次)

验收标准
- 能跑起来
- 成功下载的图片保存到本地
- 控制台输出统计结果

提交方式:评论区贴代码或 GitHub 链接


📚 总结

这一章学了 3 个核心点:

  1. async 函数自动返回 Promise,不用手动 new Promise()
  2. await 只能在 async 函数里用,它会暂停函数执行,等 Promise 结果
  3. 错误处理用 try/catch,比 .catch() 更直观;并行任务用 Promise.all/allSettled

下一章剧透:你知道 JavaScript 是怎么"一心多用"的吗?浏览器里同时跑着好多任务,但它是单线程的,到底怎么做到的?下一章「事件循环 Event Loop」会揭开这个谜底。


延伸资源
- MDN 官方文档:https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous/Promises
- 《你不知道的 JavaScript》上卷(第 4、5 章)
- 视频:B 站「async/await 入门到精通」系列

互动钩子:你在项目里用过 async/await 吗?遇到过什么奇怪的 bug?评论区聊聊,老粉优先回复!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。