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
来管理这些操作的完成状态。