JavaScript异步编程:从Callback到Async/Await的演进
JavaScript 是单线程语言,但通过异步机制能高效处理网络请求、文件读写等耗时操作。若不掌握异步编程,程序会卡死或逻辑混乱。本文手把手带你理解三种主流异步写法,学会如何避免“回调地狱”,写出清晰可维护的代码。
1. Callback:最原始的异步方案
定义:Callback(回调函数)是一个作为参数传入另一个函数的函数,在异步操作完成后被调用。
使用场景:Node.js 的早期 API、浏览器中的 setTimeout 等。
编写一个模拟网络请求的函数:
function fetchData(callback) {
setTimeout(() => {
const data = { id: 1, name: "Alice" };
callback(null, data); // 第一个参数为错误,第二个为数据
}, 1000);
}
调用它并处理结果:
- 定义 回调函数,接收错误和数据两个参数。
- 检查
err是否存在:若存在,打印 错误;否则 使用 数据。 - 调用
fetchData并传入该回调。
fetchData((err, data) => {
if (err) {
console.error("请求失败:", err);
} else {
console.log("用户信息:", data);
}
});
问题暴露:当需要连续发起多个请求(如先获取用户ID,再用ID查订单),代码会层层嵌套:
fetchUser((err, user) => {
if (!err) {
fetchOrders(user.id, (err, orders) => {
if (!err) {
fetchProducts(orders[0].productId, (err, product) => {
// 更深的嵌套...
});
}
});
}
});
这种结构被称为“回调地狱”——难以阅读、调试和修改。
2. Promise:解决嵌套问题的标准方案
定义:Promise 是一个代表异步操作最终完成或失败的对象。它有三种状态:pending(进行中)、fulfilled(成功)、rejected(失败)。
改造上面的 fetchData 函数为 Promise 版本:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = { id: 1, name: "Alice" };
// 成功时调用 resolve,失败时调用 reject
resolve(data);
}, 1000);
});
}
使用 .then() 和 .catch() 处理结果:
- 调用
fetchData(),它返回一个 Promise。 - 链式调用
.then()处理成功结果。 - 链式调用
.catch()捕获任意环节的错误。
fetchData()
.then(data => {
console.log("用户信息:", data);
return data.id; // 返回值会传给下一个 .then
})
.then(userId => {
console.log("用户ID:", userId);
})
.catch(err => {
console.error("出错了:", err);
});
关键优势:
- 链式调用避免了嵌套。
- 错误统一在
.catch中处理,无需每个步骤都检查err。
注意:.then() 中抛出异常或返回 rejected Promise,都会跳转到最近的 .catch()。
3. Async/Await:像写同步代码一样写异步
定义:async/await 是基于 Promise 的语法糖,让异步代码看起来像同步代码。
规则:
- 在函数前加
async,该函数自动返回 Promise。 - 在 Promise 前加
await,暂停执行直到 Promise 完成。
重写上述逻辑:
- 声明 一个
async函数。 - 在函数内部,用
await调用 Promise 函数。 - 用 try/catch 包裹 可能出错的
await调用。
async function getUserInfo() {
try {
const data = await fetchData();
console.log("用户信息:", data);
const userId = data.id;
console.log("用户ID:", userId);
} catch (err) {
console.error("出错了:", err);
}
}
// 调用 async 函数
getUserInfo();
处理多个依赖请求:
async function fetchFullProfile() {
const user = await fetchUser(); // 先获取用户
const orders = await fetchOrders(user.id); // 再获取订单
const product = await fetchProducts(orders[0].productId); // 最后获取商品
return { user, orders, product };
}
并发请求优化:如果多个请求互不依赖,可用 Promise.all 并行执行:
async function fetchConcurrently() {
const [user, config] = await Promise.all([
fetchUser(),
fetchAppConfig()
]);
return { user, config };
}
这比串行执行快得多,因为两个请求同时发出。
4. 三种方式对比与选择建议
选择哪种方式取决于代码复杂度和团队规范。以下是核心差异:
| 特性 | Callback | Promise | Async/Await |
|---|---|---|---|
| 可读性 | 差(易嵌套) | 中(链式) | 优(线性) |
| 错误处理 | 每层需手动检查 | 统一 .catch() |
try/catch |
| 并发支持 | 难实现 | Promise.all |
await Promise.all(...) |
| 浏览器兼容 | 所有环境 | ES6+ | ES2017+ |
实际开发建议:
- 新项目一律使用
async/await:逻辑清晰,易于维护。 - 老项目逐步迁移:将回调函数封装为 Promise,再用
await调用。 - 避免混合写法:不要在
async函数里写.then(),保持风格统一。
封装旧式回调为 Promise:
// 假设 oldApi 是一个接受回调的函数
function promisify(oldApi) {
return function (...args) {
return new Promise((resolve, reject) => {
oldApi(...args, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};
}
// 使用
const modernFetch = promisify(oldFetch);
const data = await modernFetch(url);
5. 常见陷阱与避坑指南
-
忘记
awaitasync function badExample() { const promise = fetchData(); // 忘记 await console.log(promise); // 输出 Promise 对象,不是数据! }解决:确保所有异步调用前有
await。 -
在循环中未并发处理
// 错误:串行执行,很慢 for (const id of ids) { const item = await fetchItem(id); process(item); }正确做法:先收集 Promise,再并发等待:
const promises = ids.map(id => fetchItem(id)); const items = await Promise.all(promises); items.forEach(process); -
顶层 await 限制
在普通脚本中不能直接使用await(必须在async函数内)。
解决:要么包裹在async函数中调用,要么使用模块(ESM)环境(支持顶层 await)。 -
错误未捕获导致静默失败
async function risky() { throw new Error("Oops"); } risky(); // 未处理 rejected Promise,可能无提示解决:始终用
.catch()或try/catch处理,或在顶层监听:window.addEventListener('unhandledrejection', event => { console.error('未捕获的异步错误:', event.reason); });
掌握 async/await 是现代 JavaScript 开发的必备技能。它让异步逻辑变得直观,大幅降低出错概率。从今天起,重构你的回调代码,拥抱更简洁的异步写法。

暂无评论,快来抢沙发吧!