JavaScript 异步编程:回调函数与 Promise
JavaScript 是一门单线程语言,这意味着它同一时间只能做一件事。如果在执行耗时操作(如网络请求、文件读取)时阻塞了主线程,整个页面就会像“死机”一样无法响应。为了解决这个问题,我们需要掌握异步编程的两个核心概念:回调函数与 Promise。
理解回调函数
回调函数是异步编程的基石。简单来说,它就是把一个函数作为参数传给另一个函数,等到事情做完了,再回过头来调用这个函数。
打开你的代码编辑器,输入以下代码来模拟一个耗时操作。
function getData(callback) {
console.log('1. 开始获取数据...');
// 模拟 1 秒的耗时操作
setTimeout(() => {
const data = '这是用户数据';
console.log('2. 数据获取完成');
// 调用回调函数,将数据传回去
callback(data);
}, 1000);
}
console.log('3. 代码继续向下执行...');
// 调用函数并传入回调
getData(function(result) {
console.log('4. 回调函数执行:', result);
});
运行上述代码,你会看到控制台输出的顺序是:1 -> 3 -> 2 -> 4。这说明主线程并没有等待 setTimeout 结束,而是继续往下执行了。
回调地狱的问题
当需要连续执行多个异步操作时,比如“先验证用户 -> 再获取订单 -> 再获取详情”,回调函数必须层层嵌套。代码会变成一个不断向右延伸的三角形,这就是俗称的“回调地狱”。
阅读下面的代码,感受一下这种写法带来的视觉压迫感和维护难度。
function step1(callback) {
setTimeout(() => callback('结果 A'), 500);
}
function step2(input, callback) {
setTimeout(() => callback(input + ' -> 结果 B'), 500);
}
function step3(input, callback) {
setTimeout(() => callback(input + ' -> 结果 C'), 500);
}
// 层层嵌套
step1(function(res1) {
console.log(res1);
step2(res1, function(res2) {
console.log(res2);
step3(res2, function(res3) {
console.log(res3);
// 想要在这里处理错误非常麻烦
});
});
});
为了解决这个问题,ES6 引入了 Promise。
使用 Promise 重构
Promise 是一个代表了异步操作最终完成或失败的对象。它有三种状态:
- Pending(进行中)
- Fulfilled(已成功)
- Rejected(已失败)
状态一旦改变,就不会再变。我们可以通过 .then() 方法处理成功结果,通过 .catch() 方法处理错误。
重写上面的代码,使用 Promise 将嵌套结构改为链式调用。
function promiseStep1() {
return new Promise((resolve) => {
setTimeout(() => resolve('结果 A'), 500);
});
}
function promiseStep2(input) {
return new Promise((resolve) => {
setTimeout(() => resolve(input + ' -> 结果 B'), 500);
});
}
function promiseStep3(input) {
return new Promise((resolve) => {
setTimeout(() => resolve(input + ' -> 结果 C'), 500);
});
}
// 链式调用,像流水线一样清晰
promiseStep1()
.then(res => {
console.log(res);
return promiseStep2(res); // 返回新的 Promise
})
.then(res => {
console.log(res);
return promiseStep3(res);
})
.then(res => {
console.log(res);
})
.catch(err => {
console.error('发生错误:', err);
});
对比两种写法,Promise 链式调用的逻辑是线性的,从上往下读即可,代码层级更扁平。
为了更直观地理解 Promise 的状态流转,我们可以参考下面的状态机逻辑:
实战对比:错误处理
在回调函数中,通常约定第一个参数传递错误对象(Error-first callback),但开发者很容易忘记处理错误。而在 Promise 中,任何一步发生的错误都会自动向后传递,直到被 .catch() 捕获。
创建一个包含错误的场景,观察 Promise 的容错能力。
- 定义一个会报错的函数
simulateError。 - 编写一个正常函数
simulateSuccess。 - 组合调用它们。
function simulateError() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('出错了:网络连接失败');
}, 300);
});
}
function simulateSuccess(data) {
return new Promise((resolve) => {
setTimeout(() => {
resolve('处理成功: ' + data);
}, 300);
});
}
// 即使第二步报错,我们也可以统一在最后处理
simulateError()
.then(res => {
// 这一步不会执行,因为上一步报错了
return simulateSuccess(res);
})
.then(res => {
console.log(res);
})
.catch(err => {
// 错误穿透到这里被捕获
console.error('捕获到异常:', err);
});
运行代码,你会发现控制台直接输出了捕获到的错误信息。这就是 Promise 强大的“错误冒泡”机制。
核心差异总结
为了方便记忆,我们将回调函数与 Promise 的核心区别整理如下。
| 特性 | 回调函数 | Promise |
|---|---|---|
| 代码结构 | 容易形成“回调地狱”,嵌套深 | 链式调用,结构扁平,易读 |
| 错误处理 | 需要在每层手动判断并传递错误 | 具备错误冒泡机制,统一捕获 |
| 执行控制 | 依赖外部函数调用,控制权分散 | 通过 resolve/reject 掌握状态控制 |
| 可组合性 | 难以组合,容易混乱 | 支持Promise.all/race,易于组合 |
进阶技巧:Promise.all
当需要同时发起多个独立的异步请求,并且必须等它们全部完成后才进行下一步时,使用 Promise.all。
执行以下代码,体验并行请求的高效。
const task1 = new Promise(resolve => setTimeout(() => resolve('任务 1 完成'), 1000));
const task2 = new Promise(resolve => setTimeout(() => resolve('任务 2 完成'), 2000));
const task3 = new Promise(resolve => setTimeout(() => resolve('任务 3 完成'), 1500));
// 并行执行,总耗时取决于最慢的任务(2000ms)
Promise.all([task1, task2, task3])
.then(results => {
console.log('所有任务结束:', results);
// 输出: ['任务 1 完成', '任务 2 完成', '任务 3 完成']
});
如果使用回调函数实现相同的功能,你需要手动计算计数器,代码量会翻倍且容易出错。Promise.all 让并行操作变得极其简单。

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