Node.js 事件循环 (Event Loop)
Node.js - 事件循环 (Event Loop)
Section titled “Node.js - 事件循环 (Event Loop)”Node.js 主要通过其事件驱动架构(由事件循环 (Event Loop) 驱动)实现高并发,尽管它本质上是单线程的。理解事件循环对于理解 Node.js 如何高效处理异步操作至关重要。
单线程,非阻塞 I/O
Section titled “单线程,非阻塞 I/O”与传统 Web 服务器模型通常为每个传入请求创建一个新线程(这可能会占用大量资源)不同,Node.js 使用一个主线程来处理所有客户端请求。当 Node.js 需要执行 I/O 操作(例如读取文件、进行网络请求或查询数据库)时,它不会阻塞当前线程并等待操作完成,而是将任务委托给底层系统(通常通过一个名为 libuv 的库)。
Node.js 提供一个回调函数 (callback function)(或使用 Promises / async/await)以在操作完成后执行。当底层系统处理 I/O 操作时,Node.js 的单线程可以自由地处理其他请求或执行代码的其他部分。这种非阻塞方法是 Node.js 高效的关键,特别是在 I/O 密集型应用中。
什么是事件循环?
Section titled “什么是事件循环?”事件循环是一种机制,它使 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 moduleconst EventEmitter = require('events');
// Create an instance of EventEmitterclass MyEmitter extends EventEmitter {}const myEmitter = new MyEmitter();
// Define a listener functionconst 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 dataconsole.log('About to emit event...');myEmitter.emit('customEvent', { message: 'Hello Emitter!' });console.log('Event emitted.');
// You can also register listeners that only run oncemyEmitter.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 filefs.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: 10File 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)。