一、引言
在 JavaScript 的异步编程发展史中,回调函数、Promise 乃至 Generator 都曾扮演重要角色。而 ES2017 引入的 async/await 语法,更是将异步编程推向了一个全新的高度。它本质上只是 Generator 函数的语法糖,但却极大地提升了异步代码的可读性和可维护性。本文将深入探讨 async/await 的核心作用以及在实际开发中的常见使用场景。
二、async/await 的核心作用
1. 用同步的写法处理异步代码
async/await 最大的作用就是让异步代码看起来和同步代码几乎一模一样。在传统的 Promise 链式调用中,我们需要不断使用 .then(),而 await 关键字可以暂停当前 async 函数的执行,等待 Promise 决议后继续执行,从而避免了层层嵌套和逻辑跳跃。
2. 解决回调地狱(Callback Hell)
在早期的回调函数模式中,多个异步操作需要依赖前一个异步操作的结果,导致代码向右无限缩进,形成难以维护的“回调地狱”。虽然 Promise 解决了横向缩进的问题,但链式调用依然存在逻辑割裂。async/await 以垂直的线性逻辑书写,彻底解决了这一痛点。
// 传统 Promise 链式调用
fetch('https://www.ipipp.com/api/user')
.then(res => res.json())
.then(user => fetch('https://www.ipipp.com/api/posts?userId=' + user.id))
.then(res => res.json())
.then(posts => console.log(posts))
.catch(err => console.error(err));
// async/await 同步写法
async function getUserPosts() {
try {
const userRes = await fetch('https://www.ipipp.com/api/user');
const user = await userRes.json();
const postsRes = await fetch('https://www.ipipp.com/api/posts?userId=' + user.id);
const posts = await postsRes.json();
console.log(posts);
} catch (error) {
console.error(error);
}
}3. 更优雅的错误处理
在 async/await 中,我们可以使用传统的 try...catch 语句来捕获异步操作中的错误,这使得同步代码和异步代码的错误处理逻辑得以统一,再也不用在每一个 .then() 后面跟一个 .catch()。
三、基本语法规则
1. async 函数
在函数声明前加上 async 关键字,该函数就会自动返回一个 Promise 对象。如果函数内部 return 的是一个普通值,会被 Promise.resolve() 包装;如果抛出异常,则会被 Promise.reject() 包装。
2. await 表达式
await 只能在 async 函数内部使用(顶层 await 除外)。它会等待右侧的 Promise 对象执行完毕并返回其决议值。如果右侧不是 Promise,也会将其转化为立即决议的 Promise。
async function demo() {
// await 会暂停执行,直到 Promise 决议
const result = await new Promise(resolve => {
setTimeout(() => resolve('Hello async/await'), 1000);
});
console.log(result); // 1秒后输出: Hello async/await
}四、async/await 的常见使用场景
1. 串行异步操作(存在依赖关系)
当后一个异步请求需要依赖前一个请求的结果时,await 的特性可以完美契合。这是 async/await 最典型的应用场景。
async function fetchSequentialData() {
try {
// 第一步:获取配置信息
const config = await fetchConfig('https://www.ipipp.com/api/config');
// 第二步:根据配置信息中的 apiUrl 获取详细数据
const detail = await fetchDetail(config.apiUrl);
return detail;
} catch (err) {
console.error('请求失败:', err);
}
}2. 并行异步操作(无依赖关系)
如果多个异步操作之间没有依赖关系,使用一个一个地 await 会导致执行时间累加,降低性能。此时应结合 Promise.all 来实现并发请求。
async function fetchParallelData() {
try {
// 三个请求同时发出,等待所有请求完成
const [users, articles, comments] = await Promise.all([
fetch('https://www.ipipp.com/api/users').then(res => res.json()),
fetch('https://www.ipipp.com/api/articles').then(res => res.json()),
fetch('https://www.ipipp.com/api/comments').then(res => res.json())
]);
console.log('数据全部获取成功', users, articles, comments);
} catch (error) {
console.error('其中一个请求失败:', error);
}
}3. 循环中的异步操作
在遍历数组并需要对每个元素执行异步操作时,for...of 循环配合 async/await 是最稳妥的方式。注意,Array.prototype.forEach 不支持内部的 async/await 等待。
async function processList(ids) {
// for...of 可以保证串行处理每个异步操作
for (const id of ids) {
const data = await fetch(`https://www.ipipp.com/api/item/${id}`);
console.log(data);
}
}如果需要并行处理数组中的异步操作,可以使用 map 结合 Promise.all:
async function processListParallel(ids) // map 不需要等待回调,会立即生成所有 Promise
const promises = ids.map(id => fetch(`https://www.ipipp.com/api/item/${id}`));
// 统一等待所有 Promise 完成
const results = await Promise.all(promises);
console.log(results);
}4. 类方法中的异步初始化
在面向对象编程中,类的构造函数(constructor)不能是 async 函数,也不能直接使用 await。如果需要在创建实例后进行异步初始化,可以使用工厂结合 async/await。
class Database {
constructor() {
this.connection = null;
}
async init() {
// 模拟异步连接数据库
this.connection = await connectDB('https://www.ipipp.com/db');
console.log('数据库连接成功');
}
static async create() {
const instance = new Database();
await instance.init();
return instance;
}
}
// 使用方式
async function main() {
const db = await Database.create();
// 此时 db 已经完全初始化
}五、注意事项与最佳实践
避免滥用 await:对于没有依赖关系的异步操作,不要逐个 await,必须使用
Promise.all并发执行以节省时间。错误处理不可省略:await 在遇到 reject 时会抛出异常,如果不使用
try...catch捕获,会导致后续代码无法执行,甚至引起进程崩溃。顶层 await:在 ES2022 中,模块的顶层支持直接使用 await(Top-level await),不再需要将其包裹在自执行的 async 函数中,但这仅限于 ES Module 环境。
六、结语
async/await 语法是 JavaScript 异步编程的重大进步。它不仅让代码结构更加清晰、更符合人类的线性思维,还使得错误处理变得更加自然。掌握其串行与并行的使用场景,避免常见的性能陷阱,能够极大地提升我们的代码质量与开发效率。