1.Node.js 执行环境与线程模型
Node.js 基于 libuv 库实现跨平台的异步I/O。libuv提供了事件循环、线程池、异步网络等能力。
- Node.js主线程( JS 执行线程) :运行V8引擎和事件循环。所有JS代码、回调、微任务都在此线程执行。
- 线程池 或 工作线程(Worker Threads) :默认4个线程,用于处理文件的I/O、DNS查询、CPU密集型任务(如crypto.pbkdf2、zlib 压缩)。异步操作如
fs.readFile会在线程池中完成,然后通过事件循环返回主线程。worker_threads模块允许创建真正的 JS 多线程,用于 CPU 密集型任务。 - libuv 事件循环:基于epoll、kqueue、IOCP等系统接口,跨平台异步 I/O 库,负责事件循环、线程池(默认 4 个线程)、文件系统、DNS、信号等。
- 子进程 (Child Processes) :
child_process可创建独立进程,用于隔离或执行外部命令。
Node.js启动时:创建主线程 -> 初始化V8 -> 创建libuv事件循环 -> 执行同步代码 -> 进入事件循环。
2.libuv事件循环的六个阶段(源码层面)
libuv事件循环的核心是uv_run函数(src/unix/core.c 或者 src/win/core.c)。每个循环迭代依次执行以下阶段,每个阶段都有一个 任务队列:
-
timers : 执行
setTimeout/setInterval中到期的回调(由最小堆管理)。- 维护一个最小堆(
rb_tree),存放定时器节点。每次循环开始时,取出超时的回调执行。setTimeout(fn, 0)会被延迟到至少 1ms(根据系统时钟精度)。
- 维护一个最小堆(
-
pending callbacks :执行上一轮循环中延迟到现在的I/O ****回调(如某些系统错误)。
- 大多数 I/O 回调(如
fs.read的完成)在这里执行。libuv 将已完成的事件放入队列。
- 大多数 I/O 回调(如
-
idle ,prepare:内部使用,供libuv自己处理,对JS不可见。
-
poll: 核心阶段。 获取新的I/O事件(如socket可读/可写),执行相关回调(如fs.read完成、http请求响应)。如果没有待处理任务,会阻塞等待新事件,直到超时(timers阶段的最短到期时间)。
- 执行 poll 队列中的 I/O 回调(例如 socket 可读)。
- 若队列为空,则根据后续阶段决定是否阻塞等待新事件(可设置超时时间)。
-
check :执行
setImmediate的回调。setImmediate由uv_check_t句柄实现,优先级高于 I/O 回调。 -
close callbacks:执行socket.on('close')等关闭回调。
每个阶段结束后 ,都会清空process.nextTick队列 和Promise微任务队列(注意:nextTick优先级高于微任务)。
3.Node.js事件循环的执行流程(伪代码):
scss
// libuv 核心循环简化版
int uv_run(uv_loop_t* loop) {
while(loop->alive){
uv__update_time(loop); // 更新当前时间
uv__run_timers(loop); // timers阶段
uv__run_pending(loop); // pending callbacks
uv__run_idle(loop); // idle
uv__run_prepare(loop); // prepare
uv__io_poll(loop); // poll阶段(可能阻塞)
uv__run_check(loop); // check
uv__run_closing_handlers(loop); // close callbacks
}
}
4.process.nextTick、setImmediate、Promise 微任务的特殊行为
process.nextTick:不属于事件循环的任何阶段,而是在当前阶段结束后立即执行。 如果递归调用nextTick,会阻塞事件循环。优先级高于微任务(Promise)。setImmediate:在check阶段执行,属于正常宏任务。Promise:微任务由V8管理,在每次C++回调返回前清空。但是在Node.js中,process.nextTick队列在微任务之前清空。
执行顺序:process.nextTick > Promise.then > setTimeout(timers) > setImmediate。
5.代码示例:展示阶段顺序
lua
// test-event-loop-order.cjs const fs = require ( "fs" ); setTimeout ( () => console . log ( 'setTimeout' ), 0 ); setImmediate ( () => console . log ( 'setImmediate' )); fs. readFile ( './example.txt' , () => { console . log ( "I/O callback(poll)" ); process. nextTick ( () => console . log ( 'nextTick inside I/O' )); Promise . resolve (). then ( () => console . log ( 'Promise inside I/O' )); }); process. nextTick ( () => console . log ( 'nextTick top-level' )); Promise . resolve (). then ( () => console . log ( 'Promise top-level' ))
// 输出顺序(每次可能略有差异,但总体规律):
// nextTick top-level
// Promise top-level
// setTimeout
// setImmediate
// I/O callback(poll)
// nextTick inside I/O
// Promise inside I/O
// 注意:setTimeout 和 setImmediate 在非 I/O 循环中的顺序不确定(取决于执行时间)
6.项目实战:实现一个基于libuv模型的任务调度器(模拟Node.js事件循环)
目标:用JavaScript模拟Node.js的事件循环阶段,支持定时器、immediate、nextTick、微任务、文件I/O模拟。加深对阶段切换和队列优先级的理解。
代码实现:
kotlin
class NodeEventLoopSimulator {
constructor() {
this.timers = new Map(); // id -> { callback, dueTime }
this.timerId = 1;
this.pendingCallbacks = []; // 模拟I/O回调
this.setImmediateCallbacks = [];
this.closeCallbacks = [];
this.nextTickQueue = [];
this.microtaskQueue = [];
this.isRunning = false;
this.now = () => Date.now();
}
setTimeout(cb, delayMs) {
const id = this.timerId++;
const dueTime = this.now() + Math.max(1, delayMs);
this.timers.set(id, { callback: cb, dueTime });
return id;
}
setImmediate(cb){
const id = this.timerId++;
this.setImmediateCallbacks.push({ id, callback:cb });
return id;
}
// 模拟异步 I/O (文件读取等)
addIOCallback(cb) {
this.pendingCallbacks.push(cb);
}
processNextTick(cb) {
this.nextTickQueue.push(cb);
}
run() {
this.isRunning = true;
while(this.isRunning && (this.hasWork())) {
// 1.timers阶段
const expired = [];
for(let [id, timer] of this.timers) {
if(timer.dueTime <= this.now()) {
expired.push(timer.callback);
this.timers.delete(id);
}
}
for(const cb of expired) this.executeWithMicrotasks(cb);
// 2. pending callbacks阶段
const pending = [...this.pendingCallbacks];
this.pendingCallbacks = [];
for(const cb of pending) {
this.executeWithMicrotasks(cb);
}
// 3. idle/prepare 阶段跳过
// 4. poll阶段:如果有pending I/O 或者 timers 则继续,否则模拟阻塞
// 这里简化为检查是否有定时器在将来,若有则计算等待时间(模拟阻塞)
let pollTimeout = this.computePollTimeout();
if(pollTimeout > 0) {
// 模拟阻塞等待,实际会等待新事件到来
this.sleep(Math.min(pollTimeout, 1000));
}
// 5. check阶段:setImmediate
const immediates = [...this.setImmediateCallbacks];
this.setImmediateCallbacks = [];
for(const {callback} of immediates) this.executeWithMicrotasks(callback);
// 6. close callback阶段
const closes = [...this.closeCallbacks];
this.closeCallbacks = [];
for(const cb of closes) this.executeWithMicrotasks(cb);
}
}
computePollTimeout() {
let nextTimerTime = Infinity;
for(let timer of this.timers.values()) {
nextTimerTime = Math.min(nextTimerTime, timer.dueTime - this.now());
}
if(nextTimerTime <= 0) return 0;
// 如果有 immediate 或者 I/O pending,poll 不阻塞
if(this.setImmediateCallbacks.length > 0 || this.pendingCallbacks.length > 0) return 0;
return nextTimerTime === Infinity ? 1000 : nextTimerTime;
}
sleep(ms) {
const end = this.now() + ms;
while(this.now() < end) { /* 模拟阻塞 */ }
}
executeWithMicrotasks(callback) {
// 先执行 nextTick 队列(在微任务之前)
while(this.nextTickQueue.length){
const nextTickCb = this.nextTickQueue.shift();
nextTickCb();
}
// 执行回调
callback();
// 执行微任务队列(Promise)
while(this.microtaskQueue.length) {
const microCb = this.microtaskQueue.shift();
microCb();
}
}
hasWork() {
return this.timers.size > 0 || this.pendingCallbacks.length > 0 || this.setImmediateCallbacks.length > 0 || this.closeCallbacks.length > 0 || this.nextTickQueue.length > 0 || this.microtaskQueue.length > 0;
}
// 添加微任务(模拟Promise.then)
enqueueMicrotask(cb) {
this.microtaskQueue.push(cb);
}
}
// 使用示例:
const loop = new NodeEventLoopSimulator();
loop.setTimeout(() => console.log('timer 100ms'), 100);
loop.setImmediate(() => console.log('immediate'));
loop.addIOCallback(() => console.log('I/O callback'));
loop.processNextTick(() => console.log('nextTick'));
loop.enqueueMicrotask(() => console.log('microtask'));
loop.run();
7.大厂面试深度追问
Q1: libuv的线程池默认大小是多少?如何修改?线程池处理哪些任务?
默认 4个 线程,可通过process.env.UV_THREADPOOL_SIZE修改(最大1024)。线程池用于处理异步I/O(fs、crypto.randomBytes、DNS等)。回调完成后,结果会通过事件循环的pending callbacks阶段返回给JS。
Q2: process.nextTick 递归调用会导致什么后果?事件循环能否继续?
会导致事件循环卡死,因为每个阶段切换前都会清空nextTick队列,如果nextTick不断添加自身,宏任务阶段永远得不到执行,类似于while(true)。Node.js没有硬性限制,但会警告MaxListenersExceededWarning并可能耗尽内存。
Q3: setImmediate 和 setTimeout(fn,0) 在 I/O 循环内部执行顺序有什么区别?为什么?
在I/O回调内部,setImmediate一定先于setTimeout(fn,0),因为poll阶段结束后会立即进入check阶段执行setImmediate,而定时器需要等待下一轮循环。外部代码中的顺序不确定,取决于事件循环启动时的时间。
Q4: Node.js中Promise微任务和process.nextTick哪个先执行?为什么这样设计?
process.nextTick先执行。因为Node.js设计者希望nextTick用于在同一个阶段内立即执行一些清理或者继续操作,优先级高于其他异步。微任务用于Promise的异步链式调用,次优先级。
Q5: 如何实现一个可以主动停止的事件循环?例如在特定条件下退出而不等待所有定时器?
调用 uv_stop() 可以停止当前循环。在JS层面,process.exit() 直接终止进程;或者保持一个标志,在循环条件中判断。自定义模拟器中,可以设置 this.isRunning = false 跳出while循环。
Q6: Node.js事件循环中的poll阶段阻塞等待是如何影响CPU利用率的?如何避免不必要的阻塞?
当没有待处理任务时,poll会阻塞等待新的事件(如新请求)。这可以让CPU空闲,提高系统吞吐。如果希望保持低延迟,可以设置 setImmediate 或者定时器来强制循环不阻塞。
Q7: worker_threads 中的事件循环和主线程相同吗?如何通信?
每个Worker线程有独立的libuv事件循环和V8实例。他们通过MessagePort和MessageChannel传递消息(底层是C++序列化)。主线程和Worker之间的通信不经过事件循环的I/O阶段,而是通过内部管道。