Node.js 工作机制
本教程共 76 篇 · 第 5 篇 · 更新于 2026-07-25 · 约 7 分钟阅读
5. Node.js 工作机制
本节目标:Node.js 内部是怎么跑起来的——V8 引擎怎么执行你的代码,libuv 怎么搞定异步 I/O,以及事件循环的大致轮廓。不用深挖源码,但要建立正确的心智模型。
整体架构一张图
Node.js 不是一块铁板,它分好几层。最简化的视角:
你的 JavaScript 代码
↓
Node.js 核心模块 (fs, http, net...)
↓
C++ 绑定层 (Node API)
↓
├─ V8 引擎(执行 JS)
├─ libuv(异步 I/O + 事件循环)
├─ c-ares(DNS 解析)
├─ OpenSSL(加密)
└─ zlib(压缩)
↓
操作系统内核 (epoll/kqueue/IOCP)
你写的每一行 JavaScript,最终都要靠这些底层组件配合才能完成。理解它们各自的分工,写代码时才知道瓶颈在哪、异常从哪来。
V8 引擎:把 JS 翻译成机器码
V8 是 Google 开发的 JavaScript 引擎,Chrome 浏览器也在用它。Node.js 选 V8 做执行引擎,是因为它够快。
传统的解释型语言(比如早期的 PHP、Python)是边读边执行,速度天花板很明显。V8 走的是 JIT(Just-In-Time)编译 路线:拿到 JavaScript 代码后,先快速编译成机器码执行,同时后台分析哪些代码被频繁调用(热点代码),再进一步优化成更高效的机器码。
你可以把 V8 想象成一个翻译官。第一次见面时它给你快速口译,听着还行;聊多了发现你总在说某几句话,它就开始精心准备更精准的高级翻译版本。
在 Node.js 里查看当前 V8 版本:
console.log(process.versions.v8);
V8 还负责垃圾回收(GC)。你创建的对象不再被引用时,V8 会自动回收内存。v24 的 V8 已经内置了更先进的增量标记和并发清理,大幅减少 GC 时的程序停顿。
TipV8 的 GC 策略对内存密集型应用影响很大。如果你发现程序偶尔卡顿,可能是 GC 在干活。
node --max-old-space-size=4096可以调大堆内存上限,但治标不治本,根本办法是减少内存泄漏和不必要的大对象持有。
libuv:异步 I/O 的幕后功臣
V8 只管执行 JavaScript,它不知道怎么读文件、怎么发网络请求。这些脏活累活交给 libuv——一个用 C 写的跨平台异步 I/O 库。
libuv 的核心职责:
- 提供事件循环(Event Loop)的实现
- 把文件系统、DNS、网络等操作委托给操作系统
- 管理一个线程池,处理那些操作系统不支持纯异步的 I/O(比如文件读写)
这里有个常见的误解:Node.js 的「单线程」指的是 JavaScript 代码执行在单线程里,但底层 I/O 并不是单线程完成的。libuv 会根据需要用多线程去真正执行读写操作,完成后把结果塞回事件队列,通知你的回调函数。
事件循环:Node.js 的心脏
事件循环(Event Loop)是 Node.js 能高并发的根本原因。它就是一个不断转圈的调度器,按固定顺序检查各个阶段有没有待办事项。
简化版的事件循环阶段:
┌───────────────────────────┐
│ timers 阶段 │ ← setTimeout / setInterval 到期回调
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ pending callbacks 阶段 │ ← 系统级 I/O 延迟回调
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ idle, prepare(内部用) │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ poll 阶段 │ ← 核心:获取新的 I/O 事件,执行回调
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ check 阶段 │ ← setImmediate 回调
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ close callbacks 阶段 │ ← socket 关闭等清理回调
└───────────────────────────┘
每个阶段转完后,还会去检查两个「插队队列」:
process.nextTick()的回调Promise的微任务(microtasks)
这两个队列的优先级极高,能把当前阶段插队的任务全部清完,才进入下一阶段。
执行顺序实测
光看图容易晕,跑段代码感受下:
import { setImmediate } from 'node:timers';
console.log('1. 同步代码 start');
setTimeout(() => console.log('2. timer'), 0);
setImmediate(() => console.log('3. immediate'));
Promise.resolve().then(() => console.log('4. promise'));
process.nextTick(() => console.log('5. nextTick'));
console.log('6. 同步代码 end');
输出:
1. 同步代码 start
6. 同步代码 end
5. nextTick
4. promise
2. timer
3. immediate
解读:
- 同步代码永远最先执行,这点没有任何悬念。
- 同步代码跑完后,先清
nextTick队列,再清Promise微任务队列。 - 然后进入 timers 阶段,执行
setTimeout回调。 - 最后到 check 阶段,执行
setImmediate。
Note
setTimeout(..., 0)和setImmediate的执行顺序不是绝对的。如果程序启动后很快进入 poll 阶段,setImmediate可能比setTimeout(..., 0)先执行。想深入了解可以看第 15 章,那里专门讲这三兄弟的恩怨。
阻塞事件循环的代价
事件循环转得顺,你的应用就能同时处理成千上万个请求。但如果某个回调函数里写了耗时计算,事件循环就被卡住了。
import { createServer } from 'node:http';
function heavyComputation() {
let sum = 0;
for (let i = 0; i < 1e10; i++) {
sum += i;
}
return sum;
}
createServer((req, res) => {
const result = heavyComputation();
res.end(`Result: ${result}`);
}).listen(3000);
这段代码在计算期间,服务器一个别的请求都处理不了。所有连接都干等着,这就是阻塞事件循环。
解决思路:
- 把计算拆成小块,用
setImmediate分批执行 - 用
worker_threads把计算丢到独立线程(第 35 章细讲) - 如果是视频转码这类重活,直接调外部服务或者子进程
Warning生产环境里一个常见的坑:有人把
JSON.parse用在几 MB 的大 JSON 上,或者在循环里同步读写大文件。这些操作看起来 harmless,实际上能把事件循环卡住几百毫秒,请求延迟瞬间爆炸。
线程池与文件 I/O
网络 I/O(比如 HTTP 请求)在 Linux 上可以用 epoll 纯异步完成,但文件 I/O 不一样。大多数操作系统没有提供完善的异步文件 API,所以 libuv 默认用一个 4 线程的线程池 来处理文件操作。
这意味着:如果你同时发起大量文件读写,即使代码写的是异步 API,底层也可能在排队等线程池里的线程空闲。
import { readFile } from 'node:fs';
// 这 5 个文件读操作,底层可能在 4 个线程里并发执行
// 第 5 个要稍微等一等
readFile('a.txt', cb);
readFile('b.txt', cb);
readFile('c.txt', cb);
readFile('d.txt', cb);
readFile('e.txt', cb);
可以通过环境变量 UV_THREADPOOL_SIZE 调大线程池,默认 4,最大 1024:
UV_THREADPOOL_SIZE=16 node app.js
但别无脑拉到 1024,线程也有上下文切换开销。一般情况下 16-32 够用了。
版本差异速览
不同 Node.js 版本在底层实现上有不少进化:
- v18:引入了全局
fetch,底层基于 undici,走独立的 HTTP 客户端逻辑,不再依赖旧的http.request。 - v20:V8 升级带来更好的性能,事件循环的定时器精度有改进。
- v24 LTS:V8 进一步优化,权限模型(Permission Model)更成熟,诊断工具链更完善。
- v26 Current:继续跟进最新 V8,实验性特性更多。
对于大多数开发者来说,不用关心每个版本的 V8 具体改了什么。知道「升级 Node.js = 升级 V8 = 更快 + 新语法支持」就够了。
给初学者的建议
你不需要把 libuv 的源码读一遍才能写好 Node.js。但这几个概念必须刻进脑子:
- JavaScript 执行是单线程的,别在主线程做重计算。
- I/O 操作(文件、网络、数据库)尽量用异步 API。
- 回调函数执行有先后顺序,同步 > nextTick > Promise > timers > immediate。
- 事件循环一旦卡住,全站受影响。
这些心智模型建立好了,后面学 Promise、async/await、Stream 都会轻松很多。
下一部分我们进入模块系统——这是 Node.js 工程化的基石。