Node.js事件循环核心机制

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)。每个循环迭代依次执行以下阶段,每个阶段都有一个 任务队列

  1. timers : 执行setTimeout / setInterval 中到期的回调(由最小堆管理)。

    1. 维护一个最小堆(rb_tree),存放定时器节点。每次循环开始时,取出超时的回调执行。setTimeout(fn, 0) 会被延迟到至少 1ms(根据系统时钟精度)。
  2. pending callbacks :执行上一轮循环中延迟到现在的I/O ****回调(如某些系统错误)。

    1. 大多数 I/O 回调(如 fs.read 的完成)在这里执行。libuv 将已完成的事件放入队列。
  3. idle ,prepare:内部使用,供libuv自己处理,对JS不可见。

  4. poll: 核心阶段。 获取新的I/O事件(如socket可读/可写),执行相关回调(如fs.read完成、http请求响应)。如果没有待处理任务,会阻塞等待新事件,直到超时(timers阶段的最短到期时间)。

    1. 执行 poll 队列中的 I/O 回调(例如 socket 可读)。
    2. 若队列为空,则根据后续阶段决定是否阻塞等待新事件(可设置超时时间)。
  5. check :执行setImmediate 的回调。setImmediateuv_check_t 句柄实现,优先级高于 I/O 回调。

  6. 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: setImmediatesetTimeout(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阶段,而是通过内部管道。

相关推荐
初圣魔门首席弟子6 小时前
Node.js 详细介绍(知识库版)
windows·qt·node.js·知识库
糖拌西瓜皮6 小时前
Java 开发者如何快速上手 Node.js:一份从入门到进阶的学习路线
node.js
yspwf7 小时前
NestJS 配置管理完整方案
后端·架构·node.js
网络点点滴7 小时前
Node.js事件驱动架构
架构·node.js
weixin_471383039 小时前
Node.js + Express 入门实战笔记-01-基础
node.js·lua·express
Rain5091 天前
2.2 数据基础:数据库集成与 ORM(TypeORM / Prisma)
数据库·人工智能·ai·数据分析·node.js·自动化·ai编程
大家的林语冰1 天前
npm 不忍了,正式上线“阶段式发布“的新功能,进一步对抗频繁的供应链攻击!
前端·javascript·node.js
天蓝色的鱼鱼1 天前
Node.js 现在能直接跑 TypeScript 了,tsx 和 ts-node 还需要吗?
前端·typescript·node.js
Rain5091 天前
2.3. 安全配置:环境变量与 API 密钥管理
前端·人工智能·后端·安全·ai·node.js·ai编程