Skip to content

Node.js 事件循环 (Event Loop)

Node.js 主要通过其事件驱动架构(由事件循环 (Event Loop) 驱动)实现高并发,尽管它本质上是单线程的。理解事件循环对于理解 Node.js 如何高效处理异步操作至关重要。

与传统 Web 服务器模型通常为每个传入请求创建一个新线程(这可能会占用大量资源)不同,Node.js 使用一个主线程来处理所有客户端请求。当 Node.js 需要执行 I/O 操作(例如读取文件、进行网络请求或查询数据库)时,它不会阻塞当前线程并等待操作完成,而是将任务委托给底层系统(通常通过一个名为 libuv 的库)。

Node.js 提供一个回调函数 (callback function)(或使用 Promises / async/await)以在操作完成后执行。当底层系统处理 I/O 操作时,Node.js 的单线程可以自由地处理其他请求或执行代码的其他部分。这种非阻塞方法是 Node.js 高效的关键,特别是在 I/O 密集型应用中。

事件循环是一种机制,它使 Node.js 能够执行非阻塞 I/O 操作。它是一个持续运行的进程,用于检查是否有异步操作已完成。其核心职责包括:

  • 接收来自系统或其他源的事件(例如 I/O 完成、定时器到期)。
  • 维护不同类型事件的队列(例如,定时器、I/O 回调、setImmediate、关闭处理器)。
  • 按照特定顺序(阶段)检查这些队列。
  • 执行在队列中找到的已完成事件相关的回调函数。
  • 只要有事件需要处理,就重复此循环。

可以把它想象成一个勤奋的组织者,不断检查不同的收件箱(事件队列)中是否有已完成的任务,然后执行与这些任务相关的指令(回调函数)。

使用 EventEmitter 进行事件驱动编程

Section titled “使用 EventEmitter 进行事件驱动编程”

Node.js 大量利用事件的概念。许多内置模块(如 http、fs、stream)继承自 events 模块提供的 EventEmitter 类。这个类允许对象发出命名的事件,从而触发已注册的监听器函数被调用。

这遵循了观察者模式 (observer pattern):对象(发送者 emitter)发出事件,而其他函数(监听者 listener)订阅这些事件。

// Import the events module
const EventEmitter = require('events');
// Create an instance of EventEmitter
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
// Define a listener function
const eventListener = (arg) => {
console.log('An event occurred with data:', arg);
};
// Register the listener for the 'customEvent'
myEmitter.on('customEvent', eventListener);
// Emit the 'customEvent' with some data
console.log('About to emit event...');
myEmitter.emit('customEvent', { message: 'Hello Emitter!' });
console.log('Event emitted.');
// You can also register listeners that only run once
myEmitter.once('oneTimeEvent', () => {
console.log('This listener runs only once.');
});
myEmitter.emit('oneTimeEvent');
myEmitter.emit('oneTimeEvent'); // This second emit will have no listener

运行此代码将输出:

About to emit event...
An event occurred with data: { message: 'Hello Emitter!' }
Event emitted.
This listener runs only once.

异步回调如何与事件循环协同工作

Section titled “异步回调如何与事件循环协同工作”

我们来看一个异步的文件读取操作。Node.js 遵循一个约定:异步操作的回调函数的第一个参数通常是一个错误对象(如果未发生错误则为 null),后续参数包含结果。

创建一个名为 input.txt 的文本文件,包含以下内容:

This is the content of input.txt.
Node.js will read this asynchronously.

创建一个名为 readFileExample.js 的 js 文件:

const fs = require('fs');
console.log('Starting file read...');
// Asynchronously read the file
fs.readFile('input.txt', 'utf8', (err, data) => {
// 此回调函数由事件循环执行
// *在*文件读取操作完成后执行。
console.log('File read callback executing...');
if (err) {
console.error('Error reading file:', err);
return; // 退出回调函数
}
console.log('File content:\n' + data);
});
console.log('File read initiated, moving to next line...');
// 这是一个文件读取挂起时运行的其他操作示例
let sum = 0;
for (let i = 0; i < 5; i++) {
sum += i;
}
console.log('Sync calculation done, sum:', sum);

当你运行 node readFileExample.js 时,fs.readFile 开始操作并注册回调。Node.js 不会等待,它立即执行 console.log('File read initiated...') 和同步循环。只有当文件读取完成并且事件循环处理了 I/O 完成事件后,回调函数才会被执行,打印文件内容。

预期的输出顺序:

Starting file read...
File read initiated, moving to next line...
Sync calculation done, sum: 10
File read callback executing...
File content:
This is the content of input.txt.
Node.js will read this asynchronously.

现代异步模式:虽然回调是基础,但现代 Node.js 开发通常使用 Promises 和 async/await 语法来更清晰地处理异步操作。许多核心模块现在提供了基于 Promise 的替代方案(例如 require('fs').promises)。