`async/await` 语法、复杂链式调用
本文深入解析JavaScript中的async/await语法,介绍其作为Promise语法糖如何简化异步代码编写。主要内容包括:1) async函数始终返回Promise,并能自动处理返回值或错误;2) await表达式暂停执行等待Promise结果,支持try/catch错误处理;3) 针对不同异步场景提供优化方案:顺序依赖调用使用串行await,独立操作通过Promise.all实现并行执行
·
今天,我们来深入探讨 async/await
语法以及如何处理复杂的异步链式调用。这是一个现代 JavaScript 中非常重要且强大的特性。
第一部分:async/await
语法精讲
async/await
是建立在 Promise 之上的语法糖,它让异步代码的书写和阅读更像同步代码,从而避免了“回调地狱”并简化了错误处理。
1. async
函数
- 定义: 在一个函数声明前加上
async
关键字,这个函数就成为了一个异步函数。 - 返回值:
async
函数永远返回一个 Promise。- 如果函数内部返回了一个非 Promise 的值(例如
return 42;
),async
函数会自动用Promise.resolve()
将其包装成一个已解决的 Promise。 - 如果函数内部返回了一个 Promise,那么
async
函数就直接返回这个 Promise。 - 如果函数内部抛出一个异常(
throw new Error(...)
),async
函数会返回一个被拒绝的 Promise。
- 如果函数内部返回了一个非 Promise 的值(例如
async function foo() {
return 42;
}
// 等价于:
function foo() {
return Promise.resolve(42);
}
foo().then(value => console.log(value)); // 42
async function bar() {
throw new Error('Oops!');
}
// 等价于:
function bar() {
return Promise.reject(new Error('Oops!'));
}
bar().catch(error => console.error(error)); // Error: Oops!
2. await
表达式
- 作用:
await
关键字只能在async
函数内部使用。它用于“暂停”异步函数的执行,等待一个 Promise 对象被解决(fulfilled 或 rejected),然后恢复函数的执行。 - 行为:
- 如果
await
后面的表达式是一个 Promise,它会等待这个 Promise 完成。 - 如果 Promise 被成功解决(fulfilled),
await
表达式的结果就是 Promise 的解决值(resolution value)。 - 如果 Promise 被拒绝(rejected),
await
会抛出拒绝的原因(rejection reason),就像一个同步的throw
语句一样。 - 如果
await
后面的表达式不是 Promise,它会将其转换为一个已解决的 Promise(等同于Promise.resolve(nonPromise)
),然后返回这个值本身。
- 如果
// 假设 fetchData 是一个返回 Promise 的函数
async function getData() {
try {
console.log('开始获取数据...');
const result = await fetchData(); // “暂停”,等待 fetchData 的 Promise 完成
console.log('数据获取成功:', result); // 上一行的 Promise 解决后,从这里继续执行
return result;
} catch (error) {
console.error('获取数据失败:', error); // 如果 await 的 Promise 被拒绝,会跳到这里
}
}
getData();
3. 错误处理
由于 await
会在 Promise 被拒绝时抛出异常,我们可以使用经典的 try...catch
结构来捕获错误,这使得异步代码的错误处理变得非常直观。
async function getUserProfile(userId) {
try {
const user = await fetchUser(userId); // 可能失败
const posts = await fetchUserPosts(user.id); // 可能失败
const avatar = await fetchUserAvatar(user.id); // 可能失败
return { user, posts, avatar };
} catch (error) {
// 捕获任何一个 await 发生的错误
console.error('加载用户资料失败:', error);
// 可以返回一个默认值或重新抛出错误
return { user: null, posts: [], avatar: null };
}
}
你也可以选择不在 async
函数内部处理错误,而是让调用者使用 .catch()
来处理。
getUserProfile(123)
.then(profile => console.log(profile))
.catch(err => console.error('外层捕获错误:', err));
第二部分:复杂链式调用与优化
在 async/await
出现之前,复杂的异步依赖通常会导致很深的 Promise 链。现在,我们可以用更清晰的方式组织代码。
场景:有顺序依赖的链式调用
这是 async/await
最擅长的场景。代码从上到下顺序执行,可读性极佳。
// 旧的 Promise 链
function oldWay() {
login(userCredentials)
.then(user => getUserProjects(user.id))
.then(projects => getProjectDetails(projects[0].id))
.then(details => {
console.log('第一个项目详情:', details);
})
.catch(error => {
console.error('过程中出错:', error);
});
}
// 使用 async/await (更清晰)
async function newWay() {
try {
const user = await login(userCredentials);
const projects = await getUserProjects(user.id);
const firstProjectDetails = await getProjectDetails(projects[0].id);
console.log('第一个项目详情:', firstProjectDetails);
} catch (error) {
console.error('过程中出错:', error);
}
}
场景:无顺序依赖的并行调用
如果多个异步操作之间没有依赖关系,使用 await
逐个等待会导致不必要的延迟。我们应该让它们同时发起。
// 低效写法:串行执行,总耗时 = time1 + time2 + time3
async function serialCalls() {
const result1 = await fetchData1(); // 等待这个完成...
const result2 = await fetchData2(); // ...再等待这个...
const result3 = await fetchData3(); // ...最后等待这个
return [result1, result2, result3];
}
// 高效写法:并行执行,总耗时 ≈ Max(time1, time2, time3)
async function parallelCalls() {
// 立即启动所有 Promise,但不等待
const promise1 = fetchData1();
const promise2 = fetchData2();
const promise3 = fetchData3();
// 现在使用 await 等待所有 Promise 完成
const result1 = await promise1;
const result2 = await promise2;
const result3 = await promise3;
return [result1, result2, result3];
}
// 更简洁的写法:使用 Promise.all
async function parallelCallsWithPromiseAll() {
// Promise.all 接收一个 Promise 数组,返回一个 Promise
// 当所有输入的 Promise 都成功时,它返回一个结果数组
// 如果有一个失败,整个 Promise.all 会立即拒绝
const [result1, result2, result3] = await Promise.all([
fetchData1(),
fetchData2(),
fetchData3(),
]);
return [result1, result2, result3];
}
场景:混合复杂链式调用(实战)
假设一个复杂业务逻辑:
- 用户登录。
- 登录成功后,同时获取用户基本信息和权限列表。
- 根据权限列表中的第一个权限,去获取对应的详细数据。
- 将所有获取到的数据整合返回。
async function fetchComplexUserData(credentials) {
try {
// 1. 登录 (必须第一步)
const authToken = await login(credentials);
// 2. 并行获取用户信息和权限 (无依赖关系,可同时进行)
const [userInfo, permissions] = await Promise.all([
fetchUserInfo(authToken),
fetchUserPermissions(authToken),
]);
// 3. 根据权限列表的第一个权限ID,获取详情 (依赖上一步的 permissions)
const primaryPermissionDetail = await fetchPermissionDetail(permissions[0].id);
// 4. 整合所有数据
return {
user: userInfo,
permissions: permissions,
primaryPermission: primaryPermissionDetail,
};
} catch (error) {
console.error('获取用户综合数据失败:', error);
// 可以选择在这里处理特定错误,或者直接抛出
throw error; // 让调用者知道失败
}
}
// 调用
fetchComplexUserData({ username: 'foo', password: 'bar' })
.then(data => console.log('成功:', data))
.catch(err => console.error('最终失败:', err));
总结与最佳实践
- 用
async/await
代替冗长的.then()
链: 让代码更具可读性,尤其是对于有顺序依赖的异步操作。 - 善用
try...catch
: 在async
函数内部进行错误处理。 - 无依赖的操作使用并行: 使用
Promise.all()
来并行执行独立的异步任务,极大提升性能。 - 注意
await
的位置:await
会阻塞其所在的async
函数内的后续代码执行,但不会阻塞其他函数或全局代码。合理安排await
的位置。 - 别忘了
async
: 记住,只要函数体内包含了await
,这个函数就必须用async
来声明。
通过结合 async/await
的清晰性和 Promise.all
的并行能力,你可以优雅且高效地处理任何复杂的异步编程场景。
更多推荐
所有评论(0)