Node.js 事件循环(Event Loop)详解

Node.js 是单线程的 JavaScript 运行时,但它能高效处理大量并发 I/O 操作(如网络请求、文件读写),核心机制就是事件循环 。事件循环由底层库 libuv 实现,它允许 Node.js 在不阻塞主线程的情况下处理异步任务。

为什么需要事件循环?

  • JavaScript 是单线程的,同步代码会阻塞执行。
  • 但大多数操作(如网络 I/O)是耗时的,如果阻塞主线程,程序就无法响应其他请求。
  • Node.js 将异步操作"卸载"到系统内核或线程池,完成后通过回调通知主线程。
  • 事件循环负责不断检查是否有完成的异步任务,并执行对应的回调函数,从而实现非阻塞 I/O

简单来说:主线程执行同步代码 → 遇到异步操作 → 交给 libuv 处理 → libuv 完成时将回调放入队列 → 事件循环轮询队列并执行回调。

事件循环的阶段(Phases)

Node.js 的事件循环分为 6 个主要阶段(基于 libuv),它们按顺序循环执行。每轮循环(称为一个 "tick")会依次进入这些阶段:

  1. timers(定时器阶段)

    执行已到期的 setTimeout()setInterval() 回调。

    注意:定时器不是精确的,只保证"至少"在指定时间后执行(可能因其他任务延迟)。

  2. pending callbacks(待定回调阶段)

    执行某些系统级 I/O 回调(如 TCP 错误报告)。

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

    Node.js 内部使用,仅用于准备下一个阶段。

  4. poll(轮询阶段)

    最重要、最复杂的阶段:

    • 检索新的 I/O 事件(网络、文件等)。
    • 执行 I/O 相关的回调(几乎所有异步 I/O 回调在这里处理)。
    • 如果没有定时器,会在这里阻塞等待新事件到来。
    • 如果有 setImmediate(),会尽快进入 check 阶段。
  5. check(检查阶段)

    执行 setImmediate() 回调。

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

    执行关闭事件的回调(如 socket.close())。

循环流程简图

sql 复制代码
timers → pending callbacks → idle/prepare → poll → check → close callbacks → (返回 timers)

每轮循环结束后,Node.js 会检查是否还有待处理的异步任务。如果没有,进程会优雅退出。

微任务(Microtasks)和 nextTick 的特殊处理

  • 与浏览器不同,Node.js 的微任务 (如 Promise.then()queueMicrotask())和 process.nextTick() 在每个阶段结束后、进入下一个阶段前执行
  • 优先级:process.nextTick() > Promise(微任务队列)。
  • 这意味着微任务会"插队"在宏任务(阶段回调)之间执行,确保更高优先级。

注意变化(从 Node.js 20+ / libuv 1.45.0 开始):定时器回调现在只在 poll 阶段后执行(以前可能在 poll 前后都检查)。

执行顺序示例

考虑以下代码:

javascript 复制代码
console.log('start');

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

Promise.resolve().then(() => console.log('promise'));

process.nextTick(() => console.log('nextTick'));

console.log('end');

输出通常是:

arduino 复制代码
start
end
nextTick
promise
immediate  // 或 timeout(取决于 poll 阶段)
timeout    // 或 immediate
  • 同步代码先执行:startend
  • 微任务立即执行:nextTickpromise
  • 然后进入事件循环:先 timers(timeout),或先 check(immediate),具体取决于 poll 阶段的行为。

与浏览器事件循环的区别

  • 浏览器:宏任务(macrotask,如 setTimeout)和微任务(microtask,如 Promise)交替执行,一个宏任务后清空所有微任务。
  • Node.js :有多个阶段的宏任务队列,微任务在每个阶段之间执行,更复杂,适合服务器端 I/O 密集场景。

实际建议

  • 避免在事件循环中执行 CPU 密集任务(如大循环计算),会阻塞其他回调,导致延迟。
  • 使用 process.nextTick() 或 Promise 处理高优先级异步逻辑。
  • 监控事件循环延迟(可用 perf_hooks)以优化性能。

理解事件循环是掌握 Node.js 异步编程的关键,能帮助你调试回调顺序、避免阻塞和构建高并发应用。

相关推荐
持续升级打怪中11 小时前
ES6 Promise 完全指南:从入门到精通
前端·javascript·es6
AC赳赳老秦11 小时前
前端可视化组件开发:DeepSeek辅助Vue/React图表组件编写实战
前端·vue.js·人工智能·react.js·信息可视化·数据分析·deepseek
小白冲鸭11 小时前
苍穹外卖-前端环境搭建-nginx双击后网页打不开
运维·前端·nginx
wulijuan88866611 小时前
Web Worker
前端·javascript
深念Y11 小时前
仿B站项目 前端 3 首页 整体结构
前端·ai·vue·agent·bilibili·首页
IT_陈寒11 小时前
React 18实战:这5个新特性让我的开发效率提升了40%
前端·人工智能·后端
深念Y12 小时前
仿B站项目 前端 5 首页 标签栏
前端·vue·ai编程·bilibili·标签栏·trae·滚动栏
克里斯蒂亚诺更新12 小时前
vue3使用pinia替代vuex举例
前端·javascript·vue.js
Benny的老巢12 小时前
用 Playwright 启动指定 Chrome 账号的本地浏览器, 复用原账号下的cookie信息
前端·chrome
a努力。12 小时前
京东Java面试被问:双亲委派模型被破坏的场景和原理
java·开发语言·后端·python·面试·linq