Node.js
[!abstract] 一句话定义 Node.js 是一个基于 V8 引擎和 libuv 事件循环的 JavaScript 服务端运行时——用一个单线程事件循环处理成千上万的并发 I/O,让 JavaScript 跑到了浏览器外面。
为什么需要它?
2009 年之前,JavaScript 只能活在浏览器里。如果你想写一个服务器,只能选 Python、Ruby、Java、PHP……每个请求一个线程,1000 个并发连接就需要 1000 个线程,每个线程几 MB 内存——内存先崩了。
更痛的是:前端和后端用两种语言。浏览器里写 JavaScript,服务器上写 Python/Java,团队要招两种人,代码不能复用,心智模型要切换。
Ryan Dahl 在 2009 年的柏林 JSConf 上发布了 Node.js,核心主张:JavaScript 也能写服务器,而且因为事件驱动模型,它处理高并发 I/O 天然比线程模型高效。 同一种语言从前端通到后端——这就是”全栈 JavaScript”的起点。
核心直觉
想象两家餐厅:
传统服务器(线程模型):每来一位客人就雇一个服务员专门伺候。客人看菜单时服务员站着等,吃完走了才雇下一个。100 位客人 = 100 个服务员 = 巨大的工资开销(内存)。
Node.js(事件循环模型):只有一个服务员(单线程),但她极度高效——客人 A 在看菜单时,她转身去帮客人 B 点菜;客人 B 等上菜时,她去帮客人 C 结账。她从不在任何一位客人身上干等着——所有”等待”都交给厨房(操作系统/线程池),她只负责传递结果。
这就是 Node.js 的核心:单线程 + 非阻塞 I/O + 事件循环。不是并行做很多事,而是快速切换——绝不空等。
它是怎么工作的?
整体架构
graph TB
subgraph "你的 JavaScript 代码"
APP["应用代码<br/>require('node:http') 等"]
end
subgraph "Node.js 核心"
subgraph "V8 引擎 — Google 出品"
PARSE["解析 & 编译<br/>JavaScript → 字节码 → 机器码"]
HEAP["堆内存<br/>对象分配 & 垃圾回收"]
STACK["调用栈<br/>函数执行上下文"]
end
subgraph "Node Core Bindings — C++"
BIND["C++ 绑定层<br/>V8 ↔ libuv 桥梁"]
NAPI["Node-API<br/>原生 Addon 接口"]
end
subgraph "libuv — C 事件循环库"
LOOP["事件循环<br/>6 个阶段的无限循环"]
THREADPOOL["线程池<br/>默认 4 线程"]
OS["OS 异步 I/O<br/>epoll / kqueue / IOCP"]
end
end
APP --> PARSE
APP --> STACK
STACK -->|"同步调用"| BIND
STACK -->|"异步调用"| LOOP
LOOP -->|"阻塞任务"| THREADPOOL
LOOP -->|"网络/文件"| OS
BIND --> LOOP
THREADPOOL -->|"回调结果"| LOOP
OS -->|"事件通知"| LOOP
LOOP -->|"回调入栈"| STACK
架构师注释:Node.js 的本质是一个三明治——上层是 V8(执行 JS),下层是 libuv(做 I/O),中间是 C++ 绑定层把它们粘在一起。V8 负责”算”,libuv 负责”等”。单线程只存在于 V8 的调用栈层面,底层 libuv 的线程池和 OS 异步 I/O 是多线程的。
事件循环的六个阶段
事件循环是 Node.js 的心脏,它在六个阶段之间无限循环:
flowchart LR
TIMERS["⏱ Timers<br/>setTimeout / setInterval"] --> PENDING["📋 Pending<br/>系统级回调"]
PENDING --> IDLE["💤 Idle / Prepare<br/>内部使用"]
IDLE --> POLL["📡 Poll<br/>I/O 回调<br/>核心停留阶段"]
POLL --> CHECK["✅ Check<br/>setImmediate"]
CHECK --> CLOSE["🔒 Close<br/>关闭事件回调"]
CLOSE --> TIMERS
| 阶段 | 做什么 | 例子 |
|---|---|---|
| Timers | 执行到期的 setTimeout / setInterval 回调 | 定时任务 |
| Pending | 执行推迟到下一次循环迭代的系统级回调 | TCP 错误回调 |
| Idle/Prepare | libuv 内部使用,开发者不接触 | — |
| Poll | 核心阶段:检索新 I/O 事件,执行 I/O 回调 | 文件读取完成、网络请求到达 |
| Check | 执行 setImmediate 回调 | 立即执行(但排在 I/O 之后) |
| Close | 执行关闭事件回调 | socket.on('close') |
关键理解:事件循环大部分时间停在 Poll 阶段——等待新的 I/O 事件到达。如果没有待处理的回调且没有 setImmediate,它就真的在”等”。这很高效,因为等待期间不消耗 CPU。
process.nextTick() vs setImmediate()
一个经典的迷惑点:
// nextTick:当前操作完成后立即执行,在事件循环继续之前
process.nextTick(() => console.log('nextTick'));
// setImmediate:下一个 Check 阶段执行
setImmediate(() => console.log('immediate'));
// 输出顺序:nextTick → immediate
process.nextTick()不属于事件循环的任何阶段——它在当前操作完成后、事件循环继续之前执行。本质是”插队”。setImmediate()属于 Check 阶段,名字虽然叫 “immediate” 但反而比nextTick晚执行。
单线程的真相
Node.js 声称”单线程”,这个说法需要精确理解:
| 层面 | 是否单线程 | 说明 |
|---|---|---|
| JavaScript 执行 | ✅ 单线程 | 同一时刻只有一段 JS 在执行 |
| V8 引擎内部 | ❌ 多线程 | GC、优化编译在后台线程 |
| libuv 线程池 | ❌ 多线程 | 默认 4 线程,处理文件 I/O、DNS、压缩等 |
| OS 异步 I/O | ❌ 多线程 | epoll/kqueue/IOCP 由操作系统管理 |
所以:Node.js 的”单线程”是指用户 JavaScript 代码运行在单线程上。底层 I/O 和系统调用是并行的。这就是为什么 Node.js 擅长 I/O 密集型任务(网络、文件、数据库),而不擅长 CPU 密集型任务(加密、图像处理、大量计算)——后者会阻塞唯一的事件循环。
关键组件 / 核心要素
| 组件 | 作用 | 类比 |
|---|---|---|
| V8 引擎 | 解析、编译、执行 JavaScript,管理堆内存和垃圾回收 | 发动机——把 JS 燃料变成动力 |
| libuv | 跨平台事件循环、异步 I/O、线程池 | 传动系统——把发动机连接到轮子(操作系统) |
| Node Core APIs | node:fs、node:http、node:net、node:crypto 等内置模块 | 原厂配件——不用自己造轮子 |
| C++ Bindings | V8 和 libuv 之间的胶水层 | 万向节——让两个不同系统协同工作 |
| Node-API (N-API) | C/C++ 原生 Addon 的稳定接口 | 扩展槽——允许装第三方硬件 |
| npm | 包管理和分发平台(独立于 Node.js 项目,但绑定分发) | 零件商城——大家共享的零件库 |
| Streams | 流式处理数据的统一抽象(Readable / Writable / Transform / Duplex) | 传送带——数据不用全部装进内存 |
V8 的多层编译策略
V8 之所以在长时间运行场景下性能优秀,因为它的 JIT 编译器有多层优化:
源代码 → 解析 → 字节码 (Ignition)
↓ 热点函数 detected
快速机器码 (Sparkplug)
↓ 长时间热点
优化机器码 (Maglev → TurboFan)
↓ 类型假设失败
去优化 → 回到字节码
这意味着:Node.js 启动时不算最快(需要先解释字节码),但运行时间越长、热点函数被优化的次数越多,性能就越好。这是 Bun 选择 JavaScriptCore 而非 V8 的核心原因——JSC 启动快但长期优化不如 V8。
与相关概念的关系
[!info] vs Bun 两者同为 JavaScript 运行时,但设计哲学和底层完全不同:
维度 Node.js Bun JS 引擎 V8 (Chrome) JavaScriptCore (Safari) 底层语言 C++ Zig 启动时间 ~25ms ~5ms 内存占用 ~48MB 基线 ~32MB 基线 工具链 分散(npm/jest/webpack 各装各的) 一体化(单一二进制) 长期运行 GC 极其成熟(15+ 年打磨) 仍在追赶 生态 npm 200万+ 包,几乎所有包都兼容 兼容大部分但非 100% 简单说:Node.js 是稳如磐石的基础设施,Bun 是锋利的新工具。新项目可以评估 Bun,但生产环境 Node.js 仍然是更安全的选择。
[!note] vs Deno Deno 由 Node.js 原作者 Ryan Dahl 在 2018 年创建,他公开表示 Node.js 有一些他后悔的设计决策:
- Deno 默认安全(需要显式授权才能访问文件系统/网络),Node.js 默认全权
- Deno 原生支持 TypeScript,Node.js 需要额外工具链
- Deno 使用 URL imports 替代
node_modules,Node.js 使用 npm 包管理- Deno 2.x 已回退支持 npm 包和
node_modules,实用性大幅提升三者定位:Node.js = 生态王者,Bun = 性能先锋,Deno = 安全先锋
[!tip] 被大量生态使用 Node.js 是 JavaScript 后端生态的基石:
- 前端框架:Next.js、Nuxt.js、SvelteKit 全部依赖 Node.js
- 构建工具:webpack、Vite、esbuild 以 Node.js 为宿主
- AI 工具链:Claude Code、oh-my-pi(可选 Bun)都支持 Node.js
- DevOps:Serverless 函数(AWS Lambda、Vercel)的默认运行时
典型应用场景
- Web API 服务器 — 高并发 I/O(读写数据库、调用外部 API)是 Node.js 的主场。Express/Fastify/NestJS 等框架成熟稳定
- 实时应用 — WebSocket 长连接(聊天、协作编辑、实时推送),事件循环天然适合
- Serverless 函数 — AWS Lambda、Vercel Edge Functions、Cloudflare Workers 的默认运行时
- CLI 工具 — npm 上百万个包,用 JS 写脚本工具极其方便(虽然启动不如 Bun 快)
- 构建工具 — webpack、Vite、Rollup、esbuild 都以 Node.js 为宿主环境
- 微服务 BFF 层 — 前端团队用同一种语言写 Backend-for-Frontend,降低沟通成本
常见误解与陷阱
[!danger] ❌ 误以为:Node.js 是单线程的,所以它不能利用多核 CPU ✅ 实际上:Node.js 的 JavaScript 执行是单线程的,但可以通过
cluster模块或worker_threads创建多个进程/线程来利用多核。cluster模块让多个 Node.js 进程监听同一个端口,负载由操作系统分发。生产环境几乎都用这种方式。
[!danger] ❌ 误以为:Node.js 适合所有后端场景 ✅ 实际上:Node.js 擅长 I/O 密集型(网络请求、文件操作、数据库查询),不擅长 CPU 密集型(视频编码、大量数学计算、图像处理)。一个 CPU 密集任务会阻塞整个事件循环——所有请求都卡住。解决方案是将 CPU 密集任务卸载到
worker_threads、子进程或外部服务。
[!danger] ❌ 误以为:异步 = 并行 ✅ 实际上:异步只是不阻塞等待结果,不代表多个任务在同时执行。
Promise.all([a, b])看起来像并行,但 I/O 操作的并行性来自 libuv/OS,不是 JavaScript 本身。如果两个任务都是纯 CPU 计算,Promise.all依然是串行的。
[!danger] ❌ 误以为:回调地狱是 Node.js 的问题 ✅ 实际上:回调地狱(Callback Hell)是早期 JavaScript 没有Promise/async-await 的问题,不是 Node.js 的设计缺陷。现代 Node.js 全面支持
async/await,回调地狱已经是历史。但理解事件循环机制仍然是写出正确异步代码的前提。
[!danger] ❌ 误以为:Node.js 不适合大型项目(因为 JavaScript 是动态类型) ✅ 实际上:TypeScript 已经是 Node.js 生态的标准配置。Next.js、NestJS、Prisma 等大型项目都在 TypeScript 上运行,类型安全不是障碍。Node.js 24 甚至改进了对 TypeScript 的原生支持。
延伸阅读
- 想深入理解事件循环 → Node.js 官方文档的 Event Loop 章节是最权威的参考;或研究 libuv 的
uv_run()源码 - 想理解 V8 优化原理 → 研究 V8 的多层编译管线:Ignition(字节码)→ Sparkplug(快速 JIT)→ Maglev(中层优化)→ TurboFan(峰值优化)
- 想看工程实践 → 用 Express/Fastify 写一个 REST API,对比同步 vs 异步文件读取的性能差异
- 想了解最新进展 → Node.js 24(2026 LTS)升级到 V8 13.6,新增
Float16Array、using资源管理、RegExp.escape、原生 SQLite 支持(实验性)
关联概念
前置知识:JavaScript · 事件驱动架构 同族概念:Bun · Deno · V8 引擎 · libuv 被使用于:Express · Next.js · webpack · Claude Code 相关架构:事件循环 · 异步I/O · Stream · Worker Threads