在JavaScript异步编程的发展历程中,从回调函数到Promise,再到如今广泛使用的async/await,每一次迭代都让代码的可读性和可维护性得到了显著提升。async/await作为Promise的语法糖,以同步代码的形式实现异步操作,彻底解决了"回调地狱"的痛点。本文将从概念解析、基础用法、进阶技巧到常见问题,全面介绍async/await的使用方法。
一、async/await的核心概念
async/await并非独立的异步机制,而是基于Promise的上层封装,其核心由两个关键字构成:
- async:用于声明一个函数为异步函数。异步函数的核心特性是:其返回值会自动封装为一个Promise对象。如果函数内部直接返回一个非Promise值,会被包装为resolved状态的Promise;如果抛出错误,则会被包装为rejected状态的Promise。
- await:仅能在async函数内部使用,用于等待一个Promise对象的状态变更。它会暂停当前async函数的执行,直到Promise完成(resolved)或失败(rejected),然后继续执行函数后续代码,并返回Promise的 resolved 值。如果等待的不是Promise对象,会直接返回该值本身。
简单来说,async负责"开启异步上下文",await负责"暂停并等待异步结果",二者配合实现了"同步写法做异步事"的效果。
二、基础使用步骤:从Promise到async/await
在学习async/await之前,我们先回顾一个Promise的基础案例------模拟接口请求获取用户数据:
javascript
// 模拟接口请求的Promise函数
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: `用户${id}`, age: 20 + id }); // 成功返回用户数据
} else {
reject(new Error("无效的用户ID")); // 失败返回错误
}
}, 1000);
});
}
// 使用Promise链式调用
fetchUser(1)
.then(user => {
console.log("用户信息:", user);
return fetchUser(user.id + 1); // 链式请求下一个用户
})
.then(nextUser => {
console.log("下一个用户信息:", nextUser);
})
.catch(error => {
console.log("错误:", error.message);
});
虽然Promise链式调用比回调地狱清晰,但多层链式仍显繁琐。下面用async/await重构,感受其简洁性:
步骤1:用async声明异步函数
所有使用await的代码,必须包裹在被async声明的函数内部,否则会报错。我们创建一个async函数来执行异步操作:
csharp
async function getUserInfo() {
// 内部可使用await
}
步骤2:用await等待Promise结果
在async函数内部,用await修饰Promise函数,即可直接获取resolved的值,无需使用then方法:
javascript
async function getUserInfo() {
// 等待fetchUser(1)完成,并获取结果
const user = await fetchUser(1);
console.log("用户信息:", user);
// 基于第一个结果,发起第二个请求
const nextUser = await fetchUser(user.id + 1);
console.log("下一个用户信息:", nextUser);
return { user, nextUser }; // 返回值会被包装为Promise
}
步骤3:处理错误与调用异步函数
await无法直接捕获Promise的rejected状态,需配合try/catch语句处理错误。调用async函数时,可像使用Promise一样用then/catch,也可在另一个async函数中用await:
javascript
// 方式1:用then/catch调用
getUserInfo()
.then(result => {
console.log("最终结果:", result);
})
.catch(error => {
console.log("错误:", error.message);
});
// 方式2:在另一个async函数中用await(更推荐)
async function main() {
try {
const result = await getUserInfo();
console.log("最终结果:", result);
} catch (error) {
console.log("错误:", error.message);
}
}
main();
对比Promise链式调用,async/await的代码结构更接近同步逻辑,层级清晰,可读性大幅提升。
三、进阶使用技巧
掌握基础用法后,结合以下技巧可应对更复杂的异步场景:
1. 并行执行多个异步操作
需注意:单独使用await会导致异步操作串行执行 ,如果多个异步操作之间无依赖关系,串行执行会浪费时间。此时应结合Promise.all()实现并行执行,再用await等待所有结果:
javascript
// 串行执行:总耗时约3秒(1+1+1)
async function serialRequest() {
const user1 = await fetchUser(1);
const user2 = await fetchUser(2);
const user3 = await fetchUser(3);
console.log("串行结果:", [user1, user2, user3]);
}
// 并行执行:总耗时约1秒(同时发起3个请求)
async function parallelRequest() {
// 先同时发起所有请求,获取Promise数组
const promise1 = fetchUser(1);
const promise2 = fetchUser(2);
const promise3 = fetchUser(3);
// 等待所有Promise完成
const [user1, user2, user3] = await Promise.all([promise1, promise2, promise3]);
console.log("并行结果:", [user1, user2, user3]);
}
Promise.all()的特性:所有Promise都resolved时,返回resolved结果数组;只要有一个Promise rejected,立即返回该错误,其他结果会被忽略。如果需要"即使部分失败,也要获取所有结果",可使用Promise.allSettled()。
2. 处理多个异步操作的竞争关系
如果需要"多个异步操作中,只要有一个先完成就使用其结果",可结合Promise.race()与await:
javascript
// 模拟两个不同速度的接口
function fetchFromServerA(id) {
return new Promise(resolve => setTimeout(() => resolve(`A服务器:用户${id}`), 800));
}
function fetchFromServerB(id) {
return new Promise(resolve => setTimeout(() => resolve(`B服务器:用户${id}`), 1200));
}
// 竞争获取最快的结果
async function getFastestResult(id) {
try {
const fastestResult = await Promise.race([
fetchFromServerA(id),
fetchFromServerB(id)
]);
console.log("最快结果:", fastestResult); // 输出"A服务器:用户1"
} catch (error) {
console.log("错误:", error.message);
}
}
getFastestResult(1);
3. 异步函数的错误边界处理
如果有多个独立的await操作,不想因一个错误导致整个函数中断,可给单个await操作单独添加try/catch:
typescript
async function getMultipleUsers() {
// 单独处理每个请求的错误
const user1 = await fetchUser(1).catch(err => {
console.log("获取用户1失败:", err.message);
return null; // 返回默认值,避免后续代码报错
});
const user2 = await fetchUser(2).catch(err => {
console.log("获取用户2失败:", err.message);
return null;
});
console.log("用户数据:", { user1, user2 }); // 即使user2失败,仍能获取user1
}
四、常见问题与注意事项
1. await只能在async函数中使用
这是最基础的规则,若在非async函数中使用await,会直接抛出语法错误。例如:
csharp
// 错误用法:非async函数中使用await
function wrongUsage() {
const user = await fetchUser(1); // SyntaxError: await is only valid in async functions
}
// 正确用法:声明为async函数
async function correctUsage() {
const user = await fetchUser(1);
}
2. async函数返回值一定是Promise
无论async函数内部返回什么值,最终都会被包装为Promise对象。即使返回原始值,也需要用then或await获取:
javascript
async function returnPrimitive() {
return "hello async"; // 等价于return Promise.resolve("hello async")
}
// 必须用then或await获取结果
returnPrimitive().then(res => console.log(res)); // 输出"hello async"
async function getResult() {
const res = await returnPrimitive();
console.log(res); // 输出"hello async"
}
3. 避免无意识的串行执行
如前文所述,多个无依赖的await操作若单独书写,会默认串行执行。需记住:先创建所有Promise实例,再用await等待聚合结果,避免性能浪费。
4. 处理未捕获的Promise错误
如果async函数中的await操作抛出错误,且未被try/catch或.catch()捕获,会导致"未捕获的Promise拒绝"错误。在浏览器中可能触发window的unhandledrejection事件,在Node.js中可能触发process的unhandledRejection事件,严重时会导致程序退出。因此,所有await操作都必须做好错误处理。
五、总结
async/await作为Promise的语法糖,并非替代Promise,而是让Promise的使用更简洁、更符合人类的同步思维习惯。其核心优势在于:
- 代码结构更清晰,消除了链式调用的嵌套层级;
- 错误处理更直观,可使用传统的try/catch机制;
- 调试更方便,断点可直接停留在await处,查看同步执行流程。
在实际开发中,我们应熟练掌握async/await的基础用法,并结合Promise.all()、Promise.race()等方法应对不同的异步场景,同时重视错误处理,确保代码的健壮性。