从回调地狱到优雅异步:ES8 Async/Await 完全解析
在前端开发中,异步编程是绕不开的核心知识点。从早期的回调函数,到 ES6 的 Promise,再到 ES8 推出的 Async/Await,异步代码的写法越来越优雅、可读性也越来越高。本文将结合实战代码,带你彻底搞懂 Async/Await 的使用方式和核心原理。
一、异步编程的演进
1. 回调函数:初代异步方案
最早期的异步编程依赖回调函数,比如 Node.js 中的文件读取、AJAX 请求等:
// Node.js 读取文件(回调函数版)
const fs = require('fs');
fs.readFile('./1.html', 'utf-8', (err, data) => {
if (err) {
console.error('读取失败:', err);
return;
}
console.log('文件内容:', data);
});
// AJAX 请求(回调函数版)
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.github.com/users/shunwuyu/repos');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(JSON.parse(xhr.responseText));
}
};
xhr.send();
问题:回调嵌套过深会形成「回调地狱」,代码可读性和可维护性极差。
2. Promise:异步编程的标准化
ES6 推出的 Promise 解决了回调地狱问题,将嵌套的回调改为链式调用:
// 将文件读取封装为 Promise
const fs = require('fs');
const readFilePromise = (path) => {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf-8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
};
// Promise 链式调用
readFilePromise('./1.html')
.then(data => {
console.log('文件内容:', data);
// 继续发起网络请求
return fetch('https://api.github.com/users/shunwuyu/repos');
})
.then(res => res.json())
.then(data => console.log('GitHub 仓库数据:', data))
.catch(err => console.error('出错了:', err));
改进:链式调用让异步流程更清晰,但仍有优化空间(比如多个 .then 链式调用依然不够直观)。
3. Async/Await:异步代码同步化
ES8(ES2017)推出的 Async/Await 是 Promise 的语法糖,让异步代码看起来和同步代码几乎一致,可读性达到顶峰。
二、Async/Await 核心用法
1. 基础语法
async:修饰函数,标记这是一个异步函数,异步函数的返回值会自动包装为 Promise。await:只能在async函数内部使用,等待右侧的 Promise 完成(状态变为 resolved/rejected),并返回 Promise 的结果。
2. 实战示例 1:网络请求(Fetch API)
// Async/Await 实现 GitHub 仓库数据请求
const fetchRepos = async () => {
try {
// 等待 fetch 请求完成,获取响应对象
const res = await fetch('https://api.github.com/users/shunwuyu/repos');
// 等待响应体解析为 JSON
const data = await res.json();
console.log('GitHub 仓库列表:', data);
return data; // 返回值会被包装为 Promise
} catch (err) {
console.error('请求失败:', err);
}
};
// 调用异步函数
fetchRepos();
3. 实战示例 2:文件读取(Node.js)
const fs = require('fs').promises; // Node.js 10+ 内置 Promise 版 fs
// Async/Await 读取文件
const readFileAsync = async () => {
try {
const html = await fs.readFile('./1.html', 'utf-8');
console.log('文件内容:', html);
return html;
} catch (err) {
console.error('读取文件失败:', err);
}
};
readFileAsync();
4. 错误处理
Async/Await 推荐使用 try/catch 捕获异常,替代 Promise 的 .catch():
const main = async () => {
try {
// 可能出错的异步操作
const res = await fetch('https://api.github.com/users/xxx/repos'); // 不存在的用户
const data = await res.json();
console.log(data);
} catch (err) {
// 统一捕获所有异步错误
console.error('错误信息:', err.message);
}
};
main();
三、Async/Await 核心特性
- 阻塞但不卡死线程:await 只会阻塞当前 async 函数的执行流程,不会阻塞整个 JS 主线程,其他同步代码依然可以执行。
- 返回值是 Promise:无论 async 函数内部返回什么,最终都会被包装为 Promise 对象:
async function test() {
return 123;
}
console.log(test()); // Promise { 123 }
test().then(res => console.log(res)); // 123
3.并行执行异步操作:如果多个异步操作无依赖关系,可通过 Promise.all 并行执行,提升效率:
const parallelAsync = async () => {
// 同时发起两个请求,而非串行等待
const [repoData, fileData] = await Promise.all([ fetch('https://api.github.com/users/shunwuyu/repos').then(res => res.json()),
fs.re adFile('./1.html', 'utf-8')
]);
console.log('仓库数据:', repoData);
console.log('文件数据:', fileData);
};
四、对比总结
五、实战建议
- 优先使用 Async/Await 编写异步代码,提升可读性和可维护性;
- 必须使用 try/catch 捕获异步错误,避免未处理的 Promise 异常;
- 无依赖的异步操作使用 Promise.all 并行执行,优化性能;
- 浏览器端注意兼容性(IE 不支持),可通过 Babel 转译;
- Node.js 环境建议使用内置的 Promise 版 API(如 fs.promises),避免手动封装 Promise。  总结
- Async/Await 是 ES8 推出的 Promise 语法糖,核心作用是让异步代码以同步的方式书写;
- async 标记异步函数,返回值自动包装为 Promise;await 只能在 async 函数内使用,等待 Promise 完成并返回结果;
- 实际开发中优先使用 Async/Await + try/catch 处理异步逻辑,无依赖的异步操作通过 Promise.all 并行执行以提升性能。