回调函数:异步的起点与痛点

14 阅读3分钟

上一期我们聊了为什么前端需要异步,核心结论是: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. 回调很美好……直到需要连续做几件事

假如我们要实现一个很常见的业务流程:

  1. 登录
  2. 获取用户信息
  3. 根据用户等级获取推荐商品
  4. 最后展示给用户

用回调写出来大概长这样(真实项目里经常更恐怖):

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)

当异步操作需要层层嵌套时,代码会向右缩进,形成一座“金字塔”:

这里放一张真实回调地狱的样子,大家感受一下绝望感: sync5.png 这已经不是“代码”,这是“艺术”——缩进艺术

4. 回调地狱带来的真实痛点

  1. 可读性极差:代码向右飘,核心逻辑被埋得很深
  2. 错误处理噩梦:每个层都要单独判断 err
  3. 难以维护:想加一个中间步骤?整个缩进都要重调
  4. 调试困难:断点、调用栈追踪变得非常痛苦
  5. “吞错误”很容易:只要有一层没写 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 最初都基于回调
  • 至今仍有大量遗留代码和低版本环境在使用

  • 嵌套多层后变成“回调地狱”
  • 错误处理繁琐
  • 代码组织性差,难以复用和测试

正是因为回调的这些痛点,才催生了后面一系列解决方案的诞生顺序:

回调 → Promiseasync/await

下一期我们就进入重头戏:Promise —— 它几乎完美解决了回调的绝大部分问题,也成为现代 JavaScript 异步编程的真正基石。

我们下一期见~