异步编程是 JavaScript 学习路上的一道坎。很多人能背出"回调 → Promise → async/await"这个进化路线,却说不清每一步到底解决了什么问题。这篇日记我用同一个需求——"先查用户、再查订单、再查物流"——分别用三种方式实现,让你直观看到进化的动力和方向。
一、为什么需要异步
JavaScript 是单线程的,只有一个调用栈。如果一段代码要等网络请求、读文件、定时器,它就会阻塞后面所有代码。所以这些"慢操作"都被设计成异步:你先发起,我继续干别的,等结果回来再处理。问题来了——结果回来之后,下一步该怎么写?这就是异步编程要解决的核心问题。
二、第一段:回调地狱
最早的方案是回调函数:你把"结果回来后要做的事"作为函数传进去,异步操作完成时调用它。听起来没毛病,但当你要串起三步异步操作时,代码就开始向右"嵌套"了:
getUser(userId, function(user) {
getOrders(user.id, function(orders) {
getLogistics(orders[0].id, function(logistics) {
console.log(logistics);
}, function(err) {
console.error("物流查询失败", err);
});
}, function(err) {
console.error("订单查询失败", err);
});
}, function(err) {
console.error("用户查询失败", err);
});
这就是经典的"回调地狱"(Callback Hell)。它有三个痛点:嵌套深导致阅读要"右倾"、错误处理散落在每一层里、控制流(比如并行、超时)几乎无法表达。维护这种代码是一场灾难。
三、第二段:Promise 改进
Promise 把"未来的结果"抽象成一个对象,提供 then 链式调用和统一的 catch 错误处理。同样的需求,用 Promise 写出来扁平得多:
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getLogistics(orders[0].id))
.then(logistics => {
console.log(logistics);
})
.catch(err => {
console.error("查询失败", err);
});
Promise 解决了回调地狱的两个核心痛点:嵌套被拍平成了链,错误处理统一到 catch。它还提供了 Promise.all、Promise.race 这些组合方法,让并行、超时变得可表达。
但 Promise 也不是完美的:链式调用里每一步都是 then,逻辑稍微复杂就还是要在 then 里嵌套;而且 then 里拿到的值是被"包"过的,调试时断点不如同步代码直观。
Promise 拍平了嵌套,但没消除"异步感"。代码还是一眼能看出"这里是异步",读起来和同步代码不一样。
四、第三段:async/await 最终形态
async/await 是 Promise 的语法糖,它让你能用同步的写法写异步代码。同样的需求,第三版长这样:
async function showLogistics(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const logistics = await getLogistics(orders[0].id);
console.log(logistics);
} catch (err) {
console.error("查询失败", err);
}
}
对比一下:try/catch 是同步代码的错误处理方式,这里直接复用,不用学新概念;每一行 await 拿到的就是真实值,不是被包过的 Promise;代码结构和同步代码一模一样,可读性拉满。
需要注意的是,async/await 底层仍然是 Promise,它没有引入新的并发模型。所以并发请求还是要用 Promise.all,不能傻傻地连续 await:
// 串行:慢,三个请求总耗时相加
const a = await fetchA();
const b = await fetchB();
// 并行:快,两个请求同时发
const [a, b] = await Promise.all([fetchA(), fetchB()]);
五、进化的本质:让异步读起来像同步
把三段放一起看,进化的方向非常清晰:让异步代码越来越像同步代码。回调把"未来要做的事"写成嵌套函数,Promise 把它拍平成链,async/await 让它看起来就是普通的同步语句。人脑天生更擅长读顺序执行的代码,所以这个进化方向是顺势而为。
但请记住,async/await 不是 Promise 的替代,而是它的语法糖。理解 Promise 是基础,async/await 只是让你用更顺手的方式表达同样的逻辑。所以学异步的正确顺序是:先彻底搞懂 Promise,再用 async/await 写得更舒服。跳过 Promise 直接学 async/await,遇到并发、错误传播、循环 await 就会懵。
这就是异步编程的进化。看懂这三段对比,你对 JavaScript 异步的理解就立住了。