Node 事件循环机制
Node.js 通过事件循环机制来运行 JavaScript 代码,同时提供线程池处理 I/O 操作任务。下面我们来深入探讨 Node.js 的事件循环机制。
Node.js 中的两种线程
在 Node.js 中,主要有两种线程:
事件循环线程:负责 JavaScript 代码的执行,包括模块加载(require
)、同步执行回调函数以及注册新的任务等。
线程池线程:由 C++的libuv
库实现,负责处理诸如文件 I/O、网络 I/O 等异步 I/O 操作,以及一些 CPU 密集型任务。
Node.js 的系统架构
Node.js 的系统由事件循环线程和线程池两大部分组成,分别负责任务的调度和执行。
事件循环线程作为整个 Node.js 进程的核心,负责 JavaScript 代码的执行。JavaScript 是单线程的,但通过事件循环机制,可以实现非阻塞式的异步 I/O。
线程池由libuv
提供,内部维护了一定数量的线程,用于执行异步 I/O 操作。当事件循环将异步 I/O 任务交给线程池处理后,可以继续执行后面的 JavaScript 代码,从而实现了非阻塞。
Node.js 事件循环的阶段
与浏览器不同,Node.js 的事件循环中只有宏任务队列,没有微任务队列。每一轮事件循环分为如下 6 个阶段:
Timers
:执行setTimeout
/setInterval
的回调函数。Pending callbacks
:执行某些系统操作的回调函数,如 TCP 错误类型的回调。Idle, prepare
:仅供内部使用。Poll
:执行与 I/O 相关的回调,在适当条件下会阻塞。Check
:执行setImmediate
的回调函数。Close callbacks
:执行关闭请求的回调函数,如socket.on('close', ...)
。
其中,Poll
阶段是整个事件循环中最重要的阶段。在这个阶段,事件循环会检查是否有新的异步 I/O 事件需要处理,然后将这些事件的回调函数加入Poll
队列并执行。
如果Poll
队列为空,则事件循环会检查是否有到期的Timer
回调,如果有则进入Timers
阶段并执行;如果没有,则会在Poll
阶段等待,直到有新的 I/O 事件到来或者有Timer
回调到期。
宏任务与微任务的执行顺序
虽然 Node.js 中没有微任务队列,但某些特定的 API 提供了类似微任务的功能,比如Promise.then
和process.nextTick
。
process.nextTick
的回调会在每一个阶段执行完后立即执行,优先级高于Promise
。也就是说,执行顺序为:
宏任务 -> process.nextTick -> Promise.then -> 宏任务...
举个例子:
console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise');
});
process.nextTick(() => {
console.log('nextTick');
});
console.log('end');
输出结果为:
start
end
nextTick
promise
timeout
不同 Node.js 版本的差异
在 Node.js v10 及更早版本中,微任务的清空时机是在事件循环的阶段切换时。
而从 Node.js v11 开始,与浏览器的事件循环机制趋于一致,每执行一个宏任务就会清空微任务队列。
这种差异可能会导致同样的代码在不同版本的 Node.js 中出现不同的执行结果,需要特别注意。