Promise 解决了回调地狱,让异步代码可以链式串联。但 .then() 的写法仍然需要把后续逻辑放进回调函数里,当多个异步操作有依赖关系时,代码还是会越来越长。ES2017(ES8)引入的 async/await 彻底改变了这种局面——它让你用同步代码的写法来编写异步逻辑,读起来像白描,执行起来却丝毫不阻塞。
12.3.1 async 函数:返回值为 Promise 的工厂
在函数声明前加上 async 关键字,该函数就变成了异步函数。异步函数的调用结果永远是一个 Promise 对象:
async function fetchData() {
return '数据'; // 相当于 return Promise.resolve('数据')
}
fetchData().then(data => console.log(data)); // 输出 "数据"
函数体内可以用 return 直接返回一个值,它会自动被 Promise.resolve() 包装。如果内部抛出异常,返回的 Promise 会变成 rejected 状态:
async function fail() {
throw new Error('出错了'); // 相当于 return Promise.reject(new Error('出错了'))
}
fail().catch(err => console.log(err.message)); // 输出 "出错了"
这意味着 async 函数可以像普通函数一样处理返回值和外抛异常,调用方依然使用 .then/.catch 或继续使用 await 处理。
12.3.2 await 表达式:直取 Promise 结果
await 关键字只能出现在 async 函数内部。它后面通常跟一个 Promise 对象,整个 await 表达式 会暂停当前 async 函数的执行,等待该 Promise 变为 fulfilled 状态,然后返回 Promise 的成功值;如果 Promise 变为 rejected,await 会抛出这个错误(相当于对 reject 的值做了一个隐式抛出)。
async function getUser() {
const response = await fetch('/api/user'); // 等待 fetch 完成
const data = await response.json(); // 等待 json 解析
return data;
}
在 response 回来之前,getUser 函数的执行会暂停,但主线程并不会被阻塞——事件循环依然可以处理其他任务。await 的“等待”是异步的、非阻塞的,只是 getUser 内部后续代码被推后执行。
await 后面也可以是普通值,它会立即被当成已解决的 Promise 返回:
async function example() {
const a = await 42; // 42
return a + 1;
}
12.3.3 错误处理:try-catch 回归舞台
因为 async 函数的执行体看起来像同步代码,所以标准的 try...catch 语句可以直接捕获 await 抛出的 rejection:
async function queryData() {
try {
const data = await fetch('/api/data');
return await data.json();
} catch (err) {
console.error('请求失败:', err);
// 可以在此处返回兜底数据或者重新抛出
return { default: true };
}
}
这比 Promise 链末尾加 .catch() 更加自然,尤其当有多步异步操作时,可以精确定位是哪一步出错,不用像以往那样在长长的链上追索。
如果不使用 try-catch,也可以在调用 async 函数时用 .catch() 处理错误,但习惯上在 async 函数内部统一捕获能避免遗漏。
12.3.4 串行与并行:成也 await,败也 await
用 await 书写多个异步操作时,要特别注意顺序问题。
串行执行:一个接一个
async function sequential() {
const a = await fetchA(); // 等待 A 完成
const b = await fetchB(); // 再等待 B 完成
return [a, b];
}
fetchA 和 fetchB 之间没有依赖关系,上面的写法却让它们强制串行,总耗时 = A 耗时 + B 耗时,浪费了等待时间。
并行执行:一起出发
async function parallel() {
const [a, b] = await Promise.all([fetchA(), fetchB()]);
return [a, b];
}
先用 fetchA() 和 fetchB() 分别启动两个异步任务(它们会同时发起请求),再将得到的两个 Promise 传给 Promise.all 等待一起完成。总耗时 ≈ max(A 耗时, B 耗时)。
这是 async/await 最常见的“坑”:能用并行解决的,不要写成串行。实践中可以用代码规范或 linter 规则(如 require-await)来避免无意识的性能损耗。
12.3.5 await 的限制与注意事项
- 只能在 async 函数内部使用:在普通函数或全局作用域使用
await会报语法错误。一些现代环境(如浏览器的模块顶层、Node.js 的 ES Module 顶层)开始支持 Top-level await,但仍需谨慎使用。 - 不支持
await的.then()回调:在Promise.then回调中再使用await是毫无意义的,因为回调已经在 Promise 链中了。 - 循环中的
await:在for...of或传统for循环中使用await是安全的,因为每次迭代都会等待。如果希望并发执行,应该把 Promise 收集进数组后用Promise.all。注意forEach中的异步回调不会等待循环完成,因为它内部无法感知 async 函数返回的 Promise。
12.3.6 async/await 与 Promise 的关系
async/await 没有替代 Promise,而是建立在 Promise 之上的语法糖。任何 async 函数返回的都是 Promise,你可以继续用 .then() 链式处理;在 .then 回调里也可以 return 一个 Promise 给链。两者互补共生:
- 现有 API(如
fetch)返回 Promise,天然适合await。 - 需要并发、竞速等复杂控制时,直接使用
Promise.all、Promise.race等方法。 - 封装一个返回 Promise 的工具函数,在 async 函数中
await它即可,浑然一体。
12.3.7 最佳实践
- 不要滥用 await 串行:审视图中的多个异步操作是否相互独立,独立则用
Promise.all并发。 - 总是在 async 函数中处理错误:用
try...catch包裹可能出错的await,或对函数返回的 Promise 统一添加.catch()。 - 避免 async 函数过于庞大:可以把多个
await拆分成更小的 async 函数,方便组合与出错隔离。 - 拒绝 top-level await 滥用:只在模块顶层初始化真正需要前置异步操作的场景使用(如加载全局配置),不建议在业务组件顶层无节制使用,它会延迟模块的执行。
- 理解异步本质,保留“异步思维”:看起来像同步,但执行时机仍然是异步的。注意竞态问题、多次点击产生的重复请求等场景,需要配合防抖/节流、请求取消等机制。
掌握 async/await,你就能写出既好读又好维护的异步代码。它不是银弹,但配合 Promise、错误处理和并发控制,能让复杂的异步流程像同步代码一样条理清晰。