Node.js 异步编程:回调函数与 Promise 对象
Node.js 中异步代码的必要性
Node.js 采用非阻塞、单线程架构设计,具备以下特点:
- 能够同时处理多个任务
- 不会等待耗时操作(如文件读取、数据库查询、API 调用)
设想一个场景:
如果 Node.js 在每次文件读取完成前都暂停执行,那么服务器会在每个请求时冻结。
因此,Node.js 采用异步编程模式:
- 启动任务
- 立即继续执行其他工作
- 稍后处理结果
文件读取实例对比
假设需要读取文件并输出内容。
同步方式(阻塞式)
const content = fs.readFileSync("document.txt", "utf-8");
console.log(content);
执行过程: Node.js 暂停所有操作直到文件读取完毕。
异步方式(非阻塞式)
fs.readFile("document.txt", "utf-8", (error, content) => {
if (error) throw error;
console.log(content);
});
Node.js 执行流程:
- 开始读取文件
- 继续处理其他任务
- 完成后执行回调函数
基于回调的异步执行
回调函数是作为参数传递给另一个函数,并在后续执行的函数。
示例代码
console.log("程序开始");
fs.readFile("document.txt", "utf-8", (error, content) => {
console.log("文件读取完成");
});
console.log("程序结束");
执行顺序:
- "程序开始"
- 文件读取启动(后台进行)
- "程序结束"
- 文件读取完成 → 回调函数执行 → "文件读取完成"
这就是非阻塞行为
嵌套回调的问题
考虑多个异步操作的情况
fs.readFile("file-a.txt", "utf-8", (error, contentA) => {
fs.readFile("file-b.txt", "utf-8", (error, contentB) => {
fs.readFile("file-c.txt", "utf-8", (error, contentC) => {
console.log(contentA, contentB, contentC);
});
});
});
存在问题:
- 难以阅读(金字塔结构)
- 错误处理复杂
- 调试困难
这种情况被称为:回调地狱
基于 Promise 的异步处理
Promise 的引入解决了回调函数的问题。
相同示例的 Promise 实现:
const fileSystem = require("fs").promises;
fileSystem.readFile("file-a.txt", "utf-8")
.then((contentA) => {
return fileSystem.readFile("file-b.txt", "utf-8");
})
.then((contentB) => {
return fileSystem.readFile("file-c.txt", "utf-8");
})
.then((contentC) => {
console.log(contentC);
})
.catch((error) => {
console.error(error);
});
Promise 生命周期
Promise 具有三种状态:
- 待定状态(Pending) → 初始状态
- 已解决状态(Fulfilled) → 成功
- 已拒绝状态(Rejected) → 失败
Promise 的优势
1. 更好的可读性
避免深度嵌套
2. 统一错误处理
.catch(error => console.error(error));
3. 链式调用
可以清晰地连接异步操作
4. 为 async/await 提供基础
现代 JavaScript 在 Promise 基础上构建
回调与 Promise 可读性对比
回调风格
doFirstOperation((resultOne) => {
doSecondOperation(resultOne, (resultTwo) => {
doThirdOperation(resultTwo, (resultThree) => {
console.log(resultThree);
});
});
});
Promise 风格
doFirstOperation()
.then(doSecondOperation)
.then(doThirdOperation)
.then(console.log)
.catch(console.error);
主要差异
| 特性 | 回调函数 | Promise |
|---|---|---|
| 可读性 | 差(嵌套结构) | 清晰(线性结构) |
| 错误处理 | 分散 | 集中统一 |
| 调试难度 | 困难 | 相对容易 |
| 可扩展性 | 差 | 好 |
回调执行链
开始
|
读取文件()
|
回调函数 ---> 读取文件()
|
回调函数 ---> 读取文件()
|
回调函数 ---> 结果
Promise 流程
Promise(待定)
|
已解决 ---> then()
|
已拒绝 ---> catch()
最佳实践建议
- 避免在复杂流程中使用回调
- 使用Promise或async/await
- 始终处理错误(
catch) - 优先选择:
async/await
获得更清洁的代码结构
回调函数为 Node.js 带来了异步能力,但扩展性不佳。
Promise 带来了结构化解决方案。
如今,async/await 让异步代码感觉像同步代码一样,同时保持性能优势。