MD 更新:2026/6/2

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/Preparelibuv 内部使用,开发者不接触
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 APIsnode:fsnode:httpnode:netnode:crypto 等内置模块原厂配件——不用自己造轮子
C++ BindingsV8 和 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.jsBun
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,新增 Float16Arrayusing 资源管理、RegExp.escape、原生 SQLite 支持(实验性)

关联概念

前置知识JavaScript · 事件驱动架构 同族概念Bun · Deno · V8 引擎 · libuv 被使用于Express · Next.js · webpack · Claude Code 相关架构事件循环 · 异步I/O · Stream · Worker Threads