Node.js 的事件循环允许我们在单线程中处理并发请求。事件循环的核心是将异步操作与回调函数结合起来,使得 Node.js 能够非阻塞地处理 I/O 操作。
工作原理
事件循环主要是分为几个阶段:
- Timers 阶段:处理所有定时器的回调,
setTimeout和setInterval。每当timers阶段被调用时,Node.js会检查是否有定时器到期,如果有,将执行相应的回调。 - I/O Callbacks 阶段:处理大部分的异步
I/O操作的回调。此阶段处理的回调包括文件系统I/O操作、网络请求等。 - Idle, Prepare 阶段:这个阶段主要用于内部管理,通常开发者不需要关注。
- Poll 阶段:
- 这是事件循环的核心阶段,主要负责等待
I/O事件的发生。epoll在这里发挥关键作用。 - 在这个阶段,
Node.js会检查是否有I/O事件。如果有,它会将事件的回调放入待执行的队列中。如果没有事件发生,Node.js会根据配置决定是继续等待还是转到下一个阶段。
- 这是事件循环的核心阶段,主要负责等待
- Check 阶段:处理
setImmediate的回调。此阶段的回调在Poll阶段之后立即执行。 - Close Callbacks 阶段:处理关闭事件,例如关闭
socket连接时的回调。
执行顺序
- 在事件循环的每个循环中,首先执行
timers阶段的回调,然后是I/O callbacks,接着是poll阶段,如果有微任务(例如 Promise 的回调),它们会被立即执行。 - 微任务的执行优先级高于宏任务。在同一循环中,所有微任务会在进入下一个阶段之前完成。
process.nextTick 是 Node.js 中一个重要的机制,用于在当前操作完成后立即执行回调函数。它与微任务队列密切相关:
process.nextTick的回调函数会被放入一个特殊的队列中,它的执行优先级高于微任务队列(如Promise的.then回调)。- 这意味着在当前操作完成后,如果有
process.nextTick的回调,它会在进入下一个事件循环阶段之前立即执行。 process.nextTick通常用于确保某个操作在当前执行上下文中的下一个循环中执行。它可以用来避免阻塞主线程,并在当前代码执行完后立即处理一些操作,例如错误处理、状态更新等。
举个例子:
const fs = require('fs');
console.log('Start');
// 使用 process.nextTick
process.nextTick(() => {
console.log('Next Tick 1');
});
// 使用 setTimeout
setTimeout(() => {
console.log('Timeout 1');
}, 0);
// 文件读取(I/O 操作)
fs.readFile('example.txt', 'utf8', (err, data) => {
console.log('File read complete');
});
// 使用 Promise
Promise.resolve().then(() => {
console.log('Promise 1');
});
// 再次使用 process.nextTick
process.nextTick(() => {
console.log('Next Tick 2');
});
// 结束日志
console.log('End');
答案:
Start
End
Next Tick 1
Next Tick 2
Promise 1
File read complete
Timeout 1
几种主要的 I/O 多路复用机制
select
select 是最早的 I/O 多路复用机制之一,广泛用于 POSIX 系统。它允许程序监视多个文件描述符(如 socket、文件等),以便确定哪些文件描述符可读、可写或发生了错误。
工作原理:
select 接受三个主要参数,分别是可读、可写和异常文件描述符的集合。调用时,select 会阻塞,直到至少一个文件描述符变为可用。
一旦返回,程序需要遍历文件描述符集合,检查哪些文件描述符的状态发生了变化。
poll
poll 是 select 的改进版本,解决了文件描述符数量限制的问题,支持的文件描述符数量几乎没有上限。
工作原理:
poll 使用一个数组来保存需要监视的文件描述符及其事件类型(可读、可写等)。与 select 不同,poll 在调用时不需要重设文件描述符集合。
epoll
epoll是Linux特有的 I/O 多路复用机制,设计用于处理大量并发连接,提供了比select和poll更高效的性能。
工作原理:
epoll采用事件驱动的方式,使用内核的事件通知机制。程序只需将文件描述符添加到epoll实例中,然后通过调用epoll_wait等待事件发生。- 当文件描述符的状态变化时,内核会通知应用程序,不再需要不断轮询。
IOCP (I/O Completion Ports)
I/O Completion Ports 是 Windows 平台上的高效 I/O 多路复用机制,专门为处理大量并发连接而设计。
工作原理:
使用线程池和 I/O 完成端口,允许线程从端口获取已完成的 I/O 操作。这种方法有效地将 I/O 操作与线程管理结合起来。
在
Windows下,Node.js使用的I/O多路复用机制主要是I/O Completion Ports (IOCP)。它允许多个线程共享一个或多个I/O完成端口,提供了一种高效的方式来处理异步I/O操作。IOCP结合了线程池的概念,可以自动管理和分配线程,以便在I/O操作完成时处理回调。这意味着应用程序可以更有效地利用系统资源,避免频繁的线程创建和销毁。
在
Node.js中,底层的I/O操作(例如网络请求和文件操作)是通过libuv库实现的。libuv是一个跨平台的异步I/O库,提供了对文件系统、网络和线程池的抽象。在Windows平台上,libuv会使用IOCP来处理异步I/O操作。这意味着当你在Node.js中发起一个异步I/O请求时(比如使用fs模块读取文件或通过HTTP请求获取数据),libuv会通过IOCP来管理这些操作的完成状态。