Published on

Node.js事件循环与libuv核心原理全解析(含面试题实战)

一、libuv事件循环的核心阶段(六大阶段)

libuv事件循环是Node.js实现非阻塞I/O的底层核心,按固定顺序执行各阶段任务,每个阶段对应专属回调队列:

1. timers(定时器阶段)

  • 核心作用:执行setTimeout/setInterval的到期回调函数;
  • 关键细节:定时器回调有专属队列,优先级高于I/O回调,setTimeout(fn, 0)并非立即执行,需等待本轮timers阶段或下轮循环;

示例

setTimeout(() => {
  console.log('timers阶段执行');
}, 0);

2. pending callbacks(待处理回调阶段)

  • 核心作用:执行上一轮事件循环中延迟到当前轮次的系统操作回调;
  • 典型场景:TCP连接失败的错误回调、部分操作系统异步操作的延迟回调;

示例

// 模拟TCP连接失败的回调会进入该阶段
const net = require('net');
const client = net.connect({ port: 12345 }, () => {});
client.on('error', (err) => {
  console.log('pending callbacks阶段执行'); // 连接失败回调在此阶段触发
});

3. idle/prepare(空闲/准备阶段)

  • 核心作用:libuv内部使用阶段,开发者无需关注;
  • 细节:idle用于计算事件循环空闲时间,prepare为poll阶段做准备工作;

4. poll(轮询阶段)

  • 核心作用:处理I/O事件回调(文件/网络请求等),是事件循环的核心阶段;
  • 执行逻辑
    • 若poll队列非空:同步执行队列中的I/O回调,直到队列为空或达到系统限制;
    • 若poll队列为空:
      • 存在setImmediate回调 → 直接进入check阶段;
      • setImmediate回调 → 等待新I/O事件加入队列,若有timer到期则跳回timers阶段;

示例

const fs = require('fs');
fs.readFile('./test.txt', () => {
  console.log('poll阶段执行I/O回调'); // 文件读取完成回调在此阶段触发
});

5. check(检查阶段)

  • 核心作用:专门执行setImmediate的回调函数;
  • 执行时机:poll阶段结束后立即触发,优先级高于下一轮timers阶段(若在I/O回调内同时存在setTimeoutsetImmediate);

示例

fs.readFile('./test.txt', () => {
  setTimeout(() => console.log('timers'), 0);
  setImmediate(() => console.log('check')); // 优先执行
});

6. close callbacks(关闭回调阶段)

  • 核心作用:处理I/O资源关闭的回调函数;
  • 典型场景:Socket连接断开、文件描述符关闭、stream.destroy()的回调;

示例

const net = require('net');
const server = net.createServer();
server.on('close', () => {
  console.log('close callbacks阶段执行'); // 服务器关闭回调在此阶段触发
});
server.close();

二、libuv的观察者类型及作用

观察者是libuv监听事件的“传感器”,负责将事件回调分发到对应阶段队列,主要包含五类:

1. 定时器观察者

  • 监听对象setTimeout/setInterval的到期事件;
  • 触发阶段:timers阶段;
  • 核心逻辑:维护定时器队列,按到期时间排序,到期后将回调放入timers队列。

2. I/O观察者

  • 监听对象:文件/网络描述符的I/O事件(如可读/可写);
  • 触发阶段:poll阶段;
  • 核心逻辑:基于epoll/kqueue(不同系统)实现I/O多路复用,监听事件就绪后触发回调。

3. 检查观察者

  • 监听对象setImmediate的调用事件;
  • 触发阶段:check阶段;
  • 核心逻辑setImmediate调用时注册观察者,poll阶段结束后触发回调。

4. 空闲观察者

  • 监听对象:事件循环的空闲状态;
  • 触发阶段:idle阶段;
  • 核心逻辑:事件循环空闲时触发,用于低优先级背景任务(如内存清理)。

5. 准备观察者

  • 监听对象:poll阶段的准备信号;
  • 触发阶段:prepare阶段;
  • 核心逻辑:poll阶段执行前触发,用于初始化I/O轮询的参数配置。

三、Node.js与浏览器事件循环的核心差异

特性维度Node.js事件循环(libuv)浏览器事件循环
底层实现基于libuv库(C语言),分6个阶段基于浏览器内核(Blink/WebKit),分宏/微任务
微任务执行时机每个回调结束后执行(process.nextTick优先级最高)每个宏任务执行后清空所有微任务
核心关注场景后端I/O处理(文件/网络)前端渲染(重绘/回流)+ 用户交互
特殊API支持setImmediate/process.nextTickrequestAnimationFrame/MutationObserver
定时器精度受事件循环阶段影响,精度约1ms受浏览器渲染帧率影响,精度约4ms

示例对比

// Node.js环境:先执行nextTick(微任务)→ timers → check
console.log('同步代码');
process.nextTick(() => console.log('nextTick微任务'));
setTimeout(() => console.log('timers阶段'), 0);
setImmediate(() => console.log('check阶段'));

// 浏览器环境:先执行Promise(微任务)→ setTimeout(宏任务)
console.log('同步代码');
Promise.resolve().then(() => console.log('Promise微任务'));
setTimeout(() => console.log('宏任务setTimeout'), 0);

四、经典面试题解析:I/O回调内的执行顺序

题目:分析以下代码输出顺序

const fs = require('fs');

// 场景1:I/O回调内的setTimeout vs setImmediate
fs.readFile('./test.txt', () => {
  setTimeout(() => console.log('1: setTimeout'), 0);
  setImmediate(() => console.log('1: setImmediate'));
});

// 场景2:同步代码中的setTimeout vs setImmediate
setTimeout(() => console.log('2: setTimeout'), 0);
setImmediate(() => console.log('2: setImmediate'));

解析:

  1. 场景1输出1: setImmediate1: setTimeout

    • I/O回调在poll阶段执行,执行完后poll队列为空,直接进入check阶段执行setImmediate
    • setTimeout需等待下一轮timers阶段执行。
  2. 场景2输出:顺序不确定

    • 同步代码执行时,setTimeout可能先进入timers队列,也可能setImmediate先进入check队列;
    • 取决于事件循环启动时timer是否到期,因此顺序不固定。

五、libuv事件循环的本质(伪代码)

// libuv事件循环核心逻辑伪代码
while (eventLoop.isAlive()) {
  // 1. timers阶段:执行到期定时器回调
  timersPhase.execute();
  
  // 2. pending callbacks阶段:执行延迟系统回调
  pendingCallbacksPhase.execute();
  
  // 3. idle/prepare阶段:内部准备工作
  idlePhase.execute();
  preparePhase.execute();
  
  // 4. poll阶段:处理I/O事件,可能阻塞等待
  const timeout = pollPhase.getTimeout();
  pollPhase.waitForIOEvents(timeout);
  pollPhase.execute();
  
  // 5. check阶段:执行setImmediate回调
  checkPhase.execute();
  
  // 6. close callbacks阶段:执行关闭回调
  closeCallbacksPhase.execute();
  
  // 执行所有微任务(nextTick/Promise)
  microtasks.execute();
}

Node.js 事件循环中的微任务执行时机

核心结论

微任务在每个宏任务(回调函数)执行完毕后立即执行,而不是等整个阶段或整个事件循环结束。

事件循环的 6 个阶段

   ┌───────────────────────────┐
┌─>│         timers            │  执行 setTimeout/setInterval
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │   pending callbacks       │  执行 I/O 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     idle, prepare         │  内部使用
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │         poll              │  获取新的 I/O 事件
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │         check             │  执行 setImmediate
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │    close callbacks        │  执行 close 事件
│  └─────────────┬─────────────┘
└──────────────────────────────┘

微任务执行时机详解

规则

每执行完一个宏任务(回调),就立即清空微任务队列。

示例

setTimeout(() => {
  console.log('timer1');
  Promise.resolve().then(() => console.log('promise1'));
}, 0);

setTimeout(() => {
  console.log('timer2');
  Promise.resolve().then(() => console.log('promise2'));
}, 0);

// 输出:
// timer1
// promise1    ← timer1 回调执行完,立即执行其微任务
// timer2
// promise2    ← timer2 回调执行完,立即执行其微任务

执行流程

timers 阶段:
  1. 执行 timer1 回调 → console.log('timer1')
  2. 清空微任务队列 → console.log('promise1')
  3. 执行 timer2 回调 → console.log('timer2')
  4. 清空微任务队列 → console.log('promise2')

常见误解 ❌

  1. ❌ 微任务在整个阶段的所有回调执行完才执行
  2. ❌ 微任务在整个事件循环(6个阶段)结束才执行
  3. ❌ timers 阶段的所有 setTimeout 都执行完才执行微任务

正确理解 ✅

  • ✅ 每个宏任务执行完 → 立即清空微任务队列
  • ✅ 微任务优先级高于下一个宏任务
  • ✅ 这确保了 Promise 的高优先级特性

微任务类型

  • Promise.then/catch/finally
  • process.nextTick (优先级更高,在微任务之前)
  • queueMicrotask()
  • MutationObserver

宏任务类型

  • setTimeout
  • setInterval
  • setImmediate
  • I/O 操作
  • UI 渲染(浏览器)

process.nextTick 特殊性

setTimeout(() => {
  console.log('timer');
  process.nextTick(() => console.log('nextTick'));
  Promise.resolve().then(() => console.log('promise'));
}, 0);

// 输出:
// timer
// nextTick    ← nextTick 优先于 Promise
// promise

process.nextTick 优先级高于普通微任务,在微任务队列之前执行。

关键记忆点

宏任务 → 微任务 → 宏任务 → 微任务 → ...

每执行一个宏任务,就清空一次微任务队列,然后才执行下一个宏任务。

graph TB
    Start([开始新的事件循环]) --> Timers[Timers 阶段]
    
    Timers --> TimerCallback{有 timer 回调?}
    TimerCallback -->|是| ExecTimer[执行一个 timer 回调]
    ExecTimer --> ClearMicro1[清空微任务队列]
    ClearMicro1 --> TimerCallback
    TimerCallback -->|否| Pending[Pending Callbacks 阶段]
    
    Pending --> PendingCallback{有 pending 回调?}
    PendingCallback -->|是| ExecPending[执行一个 pending 回调]
    ExecPending --> ClearMicro2[清空微任务队列]
    ClearMicro2 --> PendingCallback
    PendingCallback -->|否| Idle[Idle/Prepare 阶段]
    
    Idle --> Poll[Poll 阶段]
    Poll --> PollCallback{有 I/O 回调?}
    PollCallback -->|是| ExecPoll[执行一个 I/O 回调]
    ExecPoll --> ClearMicro3[清空微任务队列]
    ClearMicro3 --> PollCallback
    PollCallback -->|否| Check[Check 阶段]
    
    Check --> CheckCallback{有 setImmediate 回调?}
    CheckCallback -->|是| ExecCheck[执行一个 setImmediate 回调]
    ExecCheck --> ClearMicro4[清空微任务队列]
    ClearMicro4 --> CheckCallback
    CheckCallback -->|否| Close[Close Callbacks 阶段]
    
    Close --> CloseCallback{有 close 回调?}
    CloseCallback -->|是| ExecClose[执行一个 close 回调]
    ExecClose --> ClearMicro5[清空微任务队列]
    ClearMicro5 --> CloseCallback
    CloseCallback -->|否| Continue{继续循环?}
    
    Continue -->|是| Timers
    Continue -->|否| End([结束])
    
    style ClearMicro1 fill:#90EE90
    style ClearMicro2 fill:#90EE90
    style ClearMicro3 fill:#90EE90
    style ClearMicro4 fill:#90EE90
    style ClearMicro5 fill:#90EE90
    
    style ExecTimer fill:#FFE4B5
    style ExecPending fill:#FFE4B5
    style ExecPoll fill:#FFE4B5
    style ExecCheck fill:#FFE4B5
    style ExecClose fill:#FFE4B5