上一期我们聊了为什么前端需要异步,核心结论是:JavaScript 单线程 + 事件循环,让我们能把耗时操作“扔出去”,主线程继续保持响应。
而最早、最原始、最直接的异步处理方式,就是回调函数(Callback)。
今天我们就来彻底搞懂它:
它是什么?怎么用?为什么曾经是“唯一解”?
又为什么后来几乎所有人都在骂它?
1. 什么是回调函数?
简单一句话:
把**“以后要做的事”**作为参数,传给一个函数,当那个函数觉得“时机到了”,就调用你传进去的函数。
最经典的例子就是 setTimeout:
console.log("1. 开始点外卖");
setTimeout(() => {
console.log("3. 外卖到了,开吃!");
}, 2000);
console.log("2. 去刷手机了~");
输出顺序:
1. 开始点外卖
2. 去刷手机了~
3. 外卖到了,开吃!(2秒后)
这里 () => { ... } 就是回调函数,我们把它“交给” setTimeout,等 2 秒后浏览器会自动调用它。
再来几个常见的回调场景:
// DOM 事件
button.addEventListener('click', function() {
console.log('用户点击了按钮!');
});
// 数组方法
[1,2,3].forEach(item => console.log(item));
// AJAX(古老写法)
xhr.onload = function() {
console.log('数据回来了!');
};
2. 回调很美好……直到需要连续做几件事
假如我们要实现一个很常见的业务流程:
- 登录
- 获取用户信息
- 根据用户等级获取推荐商品
- 最后展示给用户
用回调写出来大概长这样(真实项目里经常更恐怖):
login('username', 'password', function(err, token) {
if (err) {
console.log('登录失败', err);
return;
}
getUserInfo(token, function(err, user) {
if (err) {
console.log('获取用户信息失败', err);
return;
}
getRecommendations(user.level, function(err, products) {
if (err) {
console.log('获取推荐失败', err);
return;
}
renderProducts(products);
});
});
});
看起来还好?
再多套两层试试看……
3. 回调地狱(Callback Hell / Pyramid of Doom)
当异步操作需要层层嵌套时,代码会向右缩进,形成一座“金字塔”:
这里放一张真实回调地狱的样子,大家感受一下绝望感:
这已经不是“代码”,这是“艺术”——缩进艺术。
4. 回调地狱带来的真实痛点
- 可读性极差:代码向右飘,核心逻辑被埋得很深
- 错误处理噩梦:每个层都要单独判断 err
- 难以维护:想加一个中间步骤?整个缩进都要重调
- 调试困难:断点、调用栈追踪变得非常痛苦
- “吞错误”很容易:只要有一层没写 return,错误就无声消失
5. 对比:如果用 Promise 会怎样?
同样的逻辑,用 Promise 链式调用大概是这样:
login('user', 'pwd')
.then(token => getUserInfo(token))
.then(user => getRecommendations(user.level))
.then(products => renderProducts(products))
.catch(err => console.log('某一步失败了', err));
是不是瞬间舒服很多?
6. 小结:回调的功与过
功:
- 最原始、最直接
- 所有异步 API 最初都基于回调
- 至今仍有大量遗留代码和低版本环境在使用
过:
- 嵌套多层后变成“回调地狱”
- 错误处理繁琐
- 代码组织性差,难以复用和测试
正是因为回调的这些痛点,才催生了后面一系列解决方案的诞生顺序:
回调 → Promise → async/await
下一期我们就进入重头戏:Promise —— 它几乎完美解决了回调的绝大部分问题,也成为现代 JavaScript 异步编程的真正基石。
我们下一期见~