Skip to content

Node.js 回调(Callbacks)概念

在 JavaScript 中,回调(callback)简单来说就是作为参数传递给另一个函数的函数,这个函数随后会在外部函数内部被调用(或“回调”)以完成某个例程或操作。

由于 Node.js 的异步特性,回调在其核心中扮演着重要角色。Node.js 执行许多操作(比如读取文件、发起网络请求或查询数据库)都是异步的。Node.js 不会等待操作完成并阻塞后续执行,而是启动操作并注册一个回调函数。当操作完成时(成功或失败),Node.js 会调用注册的回调函数,并将结果(或错误)作为参数传递进去。

这种非阻塞、事件驱动的模型使得单个 Node.js 进程能够高效地处理许多并发操作,使其在 I/O 密集型任务中具有高度的可扩展性。

约定俗成:错误优先回调 (Error-First Callbacks)

Section titled “约定俗成:错误优先回调 (Error-First Callbacks)”

Node.js 核心模块(以及许多第三方库)遵循一种称为“错误优先回调 (error-first callbacks)”的约定。在这种模式下:

  • 回调函数作为最后一个参数传递给异步方法。
  • 传递给回调函数本身的第一个参数保留用于错误对象。
  • 如果操作成功完成,这个第一个参数(error)将是 null 或 undefined。
  • 如果发生错误,error 参数将包含一个 Error 对象,其中包含有关失败的详细信息。
  • 传递给回调的任何后续参数代表操作的成功结果(例如,从文件中读取的数据)。

示例:阻塞式与非阻塞式文件读取

Section titled “示例:阻塞式与非阻塞式文件读取”

我们通过文件读取来演示它们之间的区别。首先,创建一个名为 data.txt 的文本文件:

This is some data.
Callbacks are essential in Node.js.

创建文件 blocking.js:

const fs = require('fs');
console.log('Starting blocking read...');
try {
// readFileSync 会阻塞执行,直到文件被读取完毕
const data = fs.readFileSync('data.txt', 'utf8');
console.log('File Content:\n' + data);
} catch (err) {
console.error('Error reading file:', err);
}
console.log('...Blocking read finished.');
console.log('Program Ended.');

运行它:

$ node blocking.js

输出:

Starting blocking read...
File Content:
This is some data.
Callbacks are essential in Node.js.
...Blocking read finished.
Program Ended.

注意程序是如何顺序执行的。‘Program Ended’ 只有在文件读取完成后才打印出来。

创建文件 nonblocking.js:

const fs = require('fs');
console.log('Starting non-blocking read...');
// readFile 是异步的
// 它开始读取并立即返回控制权
// 当读取完成后,回调函数会在稍后执行
fs.readFile('data.txt', 'utf8', (err, data) => {
// 这段代码在文件 I/O 完成后运行
console.log('--- Callback Executed ---');
if (err) { // 检查错误(错误优先模式)
console.error('Error reading file:', err);
return; // 在出现错误后返回很重要
}
console.log('File Content:\n' + data);
});
console.log('...Non-blocking read initiated.');
console.log('Program Ended.'); // 这一行几乎立即执行

运行它:

$ node nonblocking.js

输出:

Starting non-blocking read...
...Non-blocking read initiated.
Program Ended.
--- Callback Executed ---
File Content:
This is some data.
Callbacks are essential in Node.js.

关键观察:‘Program Ended’ 在文件内容之前打印出来。fs.readFile 函数启动了读取操作,并允许脚本的其余部分(console.log('Program Ended.'))立即执行。包含文件内容逻辑的回调函数只有在文件数据可用后才稍后执行。

在 Web 服务器中,像 readFileSync 这样的阻塞操作会阻止服务器在读取文件时处理任何其他传入请求。这会导致性能低下和响应迟缓。通过使用异步操作与回调(或 Promises / async-await),Node.js 可以在等待 I/O 操作完成的同时处理其他任务,从而有效地处理数千个并发连接。

回调地狱 (Callback Hell) 及替代方案

Section titled “回调地狱 (Callback Hell) 及替代方案”

虽然回调是基础,但将多个异步操作与回调嵌套在一起可能导致代码缩进过深,难以阅读,通常被称为“回调地狱 (Callback Hell)”或“厄运金字塔 (Pyramid of Doom)”。

现代 JavaScript 提供了更清晰地管理异步复杂性的替代方案:

  • Promises:表示异步操作最终完成(或失败)的对象。它们允许链式调用(.then()、.catch()、.finally())。
  • Async/Await:构建在 Promises 之上的语法糖,允许你编写看起来更像同步代码的异步代码,通常更容易阅读和推理(async 函数、await 关键字)。

理解回调至关重要,因为 Promises 和 async/await 都是构建在这些底层异步模式之上的。许多 Node.js 核心 API 现在也提供了基于 Promise 的版本(例如,require('fs').promises)。