一句话定义
Node.js = V8 引擎(执行 JS)+ libuv(异步 I/O)+ C++ Binding(系统能力桥接)
它不是一门新语言,而是一个让 JavaScript 突破浏览器沙箱、直接操控操作系统资源的运行时。
1. 全链路鸟瞰:一行代码穿越五层
javascript
const http = require('http');
http.createServer(handler).listen(8080);
scss
┌─────────────────────────────────────────────────────────────────┐
│ 第①层:用户代码(JS) require('http') → 模块加载 │
├─────────────────────────────────────────────────────────────────┤
│ 第②层:JS 内置模块(lib/) http.js → net.js → API 组装 │
├─────────────────────────────────────────────────────────────────┤
│ 第③层:C++ Binding(src/) internalBinding('tcp_wrap') │
│ BaseObject → AsyncWrap → HandleWrap → LibuvStreamWrap │
│ V8 Internal Field[0]=类型标记 Internal Field[1]=C++ 指针 │
├─────────────────────────────────────────────────────────────────┤
│ 第④层:libuv(deps/uv/) uv_tcp_init → uv_tcp_listen │
│ → epoll_ctl / kevent / CreateIoCompletionPort │
├─────────────────────────────────────────────────────────────────┤
│ 第⑤层:操作系统内核 socket() → bind() → listen() │
└─────────────────────────────────────────────────────────────────┘
2. 启动引导(Bootstrap)
进程启动后,在执行用户代码之前,完成三步关键工作:
scss
node 进程启动
│
├─① RegisterBuiltinBindings()
│ 宏展开 NODE_BUILTIN_BINDINGS(V) → 调用 ~70 个 _register_xxx()
│ 每个调用 node_module_register(),头插法加入 modlist_internal 链表
│
├─② CreateInternalBindingTemplates()
│ 为 ~20 个 per-isolate binding 预创建 V8 ObjectTemplate
│
└─③ Bootstrap(realm.js)
C++ 创建 JS 函数 getInternalBinding = binding::GetInternalBinding
调用 realm.js 传入 (process, getLinkedBinding, getInternalBinding, primordials)
JS 层包装为带缓存的闭包:
const bindingObj = {};
internalBinding = (module) => {
return bindingObj[module] ??= getInternalBinding(module);
};
setInternalLoaders() 将闭包存回 C++ Realm
每个内置 JS 模块编译时注入 internalBinding 到函数签名
internalBinding() 是 JS 通往 C++ 的唯一通道------它不是全局变量,而是编译期注入到每个内置模块作用域的参数,用户代码无法访问。
3. Binding:JS ↔ C++ 双向桥接
3.1 C++ 层存在的根本原因
JS 运行在 V8 沙箱中,天生无法访问操作系统。C++ 层承担六大不可替代的职责:
| 职责 | 举例 | 说明 |
|---|---|---|
| 系统调用代理 | fs.readFile() → open()/read()/close() |
JS 无法执行 syscall |
| 高性能计算 | SHA-256 + AES-NI 硬件加速 | 比纯 JS 快 10-100x |
| 事件循环集成 | libuv = 纯 C 库 | JS 无法调用 C API |
| 零拷贝内存 | Buffer 共享 V8 外堆内存 | 无需序列化/拷贝 |
| 安全隔离 | C++ 层强制权限检查 | JS 层可被篡改 |
| C 库集成 | OpenSSL / zlib / c-ares / SQLite | JS 无法运行 C 库 |
没有 C++ 层,Node.js 只是纯计算引擎,无法与外部世界交互。没有 JS 层,底层能力无法被易用。两者缺一不可。
3.2 双向通信机制
JS → C++(调用)
scss
tcp.listen(8080)
→ V8 FunctionCallback
→ TCPWrap::Listen(args)
① Unwrap<TCPWrap>(args.This()) // 从 Internal Field[1] 取出 C++ 指针
② args[0].As<Int32>()->Value() // 提取 JS 参数
③ uv_tcp_listen(&handle_, ...) // 调用 libuv
④ args.GetReturnValue().Set(err) // 返回结果
C++ → JS(回调)
cpp
// libuv 连接就绪 → C++ 回调 → JS
void TCPWrap::OnConnection(uv_stream_t* handle, int status) {
TCPWrap* wrap = ContainerOf(&TCPWrap::handle_, handle); // 从 uv handle 反查
Local<Object> obj = wrap->object(); // 获取关联 JS 对象
wrap->MakeCallback(env->onconnection_string(), 1, argv); // 回调 JS
}
3.3 BaseObject:JS↔C++ 对象关联核心
arduino
┌────────────────────────────┐ C++ 继承层次:
│ V8 JavaScript Object │ BaseObject
│ ┌──────────────────────┐ │ └─ AsyncWrap(异步追踪)
│ │ Field[0]: 类型标记 │ │ └─ HandleWrap(libuv 生命周期)
│ │ Field[1]: C++ this* │ │ └─ LibuvStreamWrap(流式 I/O)
│ └──────────────────────┘ │ ├─ TCPWrap (uv_tcp_t)
└────────────────────────────┘ ├─ PipeWrap (uv_pipe_t)
└─ TTYWrap (uv_tty_t)
构造时:SetInternalFields(object, this) → 存入 C++ 指针
反向取:Unwrap<T>(obj) → GetAlignedPointerFromInternalField(kSlot)
4. 事件循环:一切异步行为的心跳
4.1 驱动核心
cpp
// src/api/embed_helpers.cc
Maybe<ExitCode> SpinEventLoopInternal(Environment* env) {
bool more;
do {
uv_run(env->event_loop(), UV_RUN_DEFAULT); // 运行 libuv 一轮
platform->DrainTasks(isolate); // 排空 V8 平台任务(Promise 等)
more = uv_loop_alive(env->event_loop());
if (!more) EmitProcessBeforeExit(env); // beforeExit 可能产生新异步
more = uv_loop_alive(env->event_loop());
} while (more && !env->is_stopping());
return EmitProcessExitInternal(env);
}
退出条件:无活跃 handle/request,且 beforeExit 未创建新异步操作。
4.2 七个阶段
arduino
┌─► timers 单个 uv_timer_t + JS 优先队列管理
│ pending callbacks 上一轮遗留的 I/O 回调
│ idle / prepare 内部使用
│ poll ★ epoll/kqueue/IOCP 阻塞等待 I/O 事件
│ check 单个 uv_check_t + JS 链表管理 setImmediate
│ close callbacks 句柄关闭回调
└───┘ 循环
4.3 I/O 多路复用:epoll / kqueue / IOCP
事件循环 poll 阶段的性能瓶颈在此。libuv 屏蔽差异,但理解底层是掌握并发的关键:
css
select/poll: O(总 fd 数) --- 遍历全部 fd 检查就绪,高并发下极慢
epoll: O(就绪数) --- 红黑树存监控 fd + 就绪链表,只返回有事件的
kqueue: O(就绪数) --- 通用过滤器(fd/信号/文件/进程/定时器),一套 API
IOCP: O(就绪数) --- 完成模型(内核搬运数据),本质不同
就绪模型 vs 完成模型:
bash
epoll/kqueue(就绪模型):注册 fd → 等通知"可读" → 应用自己 read()
IOCP(完成模型):发起 I/O → 内核后台搬运 → 等通知"已完成,数据在 buffer"
libuv 编译期条件分支选择后端,所有后端统一将"就绪 watcher 加入 pending_queue",上层完全不感知。
4.4 epoll 全链路:从 fd 注册到回调执行(以 Linux 为例)
理解 epoll 是掌握 Node.js 高并发模型的关键。以下是 libuv 视角的完整链路:
css
① 初始化: uv_run() 前 → epoll_create1() 创建 epfd
内核分配 eventpoll:红黑树(rbr) + 就绪链表(rdlist)
整个进程共享一个 epoll 实例
② 产生fd: listen() → listen-fd → uv_io_t(listen, EPOLLIN)
accept() → conn-fd → uv_io_t(conn, EPOLLIN)
③ 注册: epoll_ctl(EPOLL_CTL_ADD, fd, {EPOLLIN|EPOLLET})
ev.data.ptr = uv_io_t* ← 关键:存指针而非 fd
内核创建 epitem 放入红黑树,绑定 ep_poll_callback
④ 等待: epoll_wait() 阻塞 → 进程休眠,不占 CPU
网卡中断 → 数据包入内核缓冲区 → ep_poll_callback
→ epitem 加入就绪链表 → 唤醒进程
⑤ 分发: epoll_wait 返回 events[]
pe->data.ptr → uv_io_t* → w->cb() 执行回调
ET 模式:循环 read() 直到 EAGAIN
⑥ 写响应: 缓冲区满 → epoll_ctl(EPOLL_CTL_MOD, EPOLLOUT)
等可写 → 继续发送
⑦ 关闭: epoll_ctl(EPOLL_CTL_DEL) → close(fd) → fd 归还
边缘触发(ET) vs 水平触发(LT) :libuv 默认使用 EPOLLET。LT 在缓冲区有数据时每次都返回,易重复触发;ET 仅在状态变化时触发一次,要求就绪后必须循环读完(直到 EAGAIN),这正是 libuv 内部读取逻辑的根源。
关键设计 :ev.data.ptr = uv_io_t* 让 libuv 无需自维护 fd→回调 映射表,内核就绪时直接返还完整上下文。
4.5 两条异步 I/O 路径
| 路径 | 机制 | 场景 | 原因 |
|---|---|---|---|
| 真正异步 I/O | epoll/kqueue/IOCP | 网络 socket、pipe | socket-fd 支持非阻塞,可被 epoll 监听 |
| 线程池模拟异步 | libuv 线程池(默认 4 线程) | 文件读写、DNS、加密 | 磁盘文件 fd 不支持 epoll------即使注册也永远返回就绪,libuv 只能交给线程池阻塞读取 |
fs.readFile → libuv 线程池 → 阻塞读磁盘 → 完成后放入 pending 队列 → 主线程执行回调
磁盘文件走线程池、Socket 走 epoll------这是 Node.js 两类 I/O 性能特征差异的根本原因。
4.6 Microtask 优先级
scss
process.nextTick() → node 队列,最高(每阶段之间排空)
Promise.then() → V8 microtask,次之
setImmediate() → check 阶段,最低
5. I/O 统一本质:一切皆 fd
5.1 fd 的内核三层结构
fd 不只是"文件描述符"------它是 Linux 统一抽象万物的句柄。内核为 fd 构建了三层结构:
scss
进程 task_struct
└─ files_struct(进程打开文件表)
├─ fd=0 → struct file(0) → inode(stdin)
├─ fd=1 → struct file(1) → inode(stdout)
├─ fd=2 → struct file(2) → inode(stderr)
├─ fd=3 → struct file(3) → disk_inode(磁盘文件 test.txt)
├─ fd=4 → struct file(4) → socket_inode(TCP 连接)
└─ fd=5 → struct file(5) → pipe_inode(管道)
| 层级 | 作用域 | 本质 |
|---|---|---|
| fd | 进程私有 | 数组下标,仅当前进程有效 |
| struct file | 内核态,每次打开新建 | 封装 f_op(read/write/close)、f_pos(读写偏移)、f_flags |
| inode | 内核全局 | 资源本体:磁盘文件存磁盘 inode,Socket/Pipe 存内存匿名 inode |
关键认知 :HTTP 连接产生的 fd 对应内存 inode(socket_inode),磁盘不会产生任何实体文件。对 Node.js 来说,write(fd) 写文件和写 Socket 调用完全相同------差异由内核 f_op 透明处理。
经典示例 :两次 fs.open("a.txt") → fd=3 和 fd=4 各自拥有独立的 struct file(独立读写偏移),但共享同一个 inode。inode 是资源本体,struct file 是打开会话。
5.2 fd 生命周期与 EMFILE
fd 不是无限资源。Linux 有两级限制:
| 限制 | 作用域 | 耗尽后果 |
|---|---|---|
ulimit -n(默认 1024) |
单进程 | 报 EMFILE(Too many open files) |
/proc/sys/fs/file-max |
系统全局 | 全局 fd 耗尽 |
默认 1024 意味着:fd 0/1/2 占用后仅剩 1021 个可用。若 1000 条 HTTP keep-alive 长连接同时存在,再新建连接即报 EMFILE。生产环境应调至 65535 甚至更高。
fd 释放时机:close(fd) → struct file 引用计数归零 → 销毁 → fd 编号归还空闲列表可复用。
inode 引用计数 :只要还有 struct file 指向 inode,inode 就不会被回收。即使 rm 删除了磁盘文件,只要进程仍持有该 fd,inode 和数据块依然保留------关闭后才真正释放。
Node.js 生产环境 EMFILE 三大元凶:
- HTTP 长连接堆积 :keep-alive 不断开,fd 持续占用 → 配置
server.timeout主动断开 - 代码漏关文件 :
fs.open()后忘记close() - CLOSE-WAIT 泄漏 :客户端断网/休眠发 FIN,内核收到但 Node 业务代码未感知、不调用
close(),socket 处于 CLOSE-WAIT,fd 永久占用------这是典型的 fd 内存泄漏
5.3 fd 继承:所有进程通信的起点
scss
fork() 后:父子进程的 fd 3 指向同一个 pipe_inode ← 这就是管道通信的根源
5.4 管道、IPC、Socket 统一模型
所有进程间通信的本质是同一件事:通过内核管理的共享资源,在进程之间传递字节。
| 方式 | 内核对象 | 寻址 | 数据传输 |
|---|---|---|---|
| 匿名管道 pipe | pipe_inode(64KB 环形缓冲) | fork 继承 | 内核内存拷贝 |
| Unix Domain Socket | socket_inode (AF_UNIX) | 文件路径 | 内核内存拷贝(无协议开销) |
| TCP Socket | socket_inode (AF_INET) | IP:Port | TCP/IP 协议栈 → 物理网络 |
lua
Unix Socket (同机): write → 内核缓冲区拷贝 → read 极快
TCP loopback (同机): write → TCP 封装 → loopback → TCP 拆封 即使同机也走完整协议栈
TCP 跨机: write → TCP/IP → 物理网络 → TCP/IP → read
网络通信就是跨机器的 IPC ------内核统一(struct file)、API 统一(read/write)、Node.js 统一(net.Socket 同时用于 TCP 和 Unix Socket)。
5.5 Node.js IPC 与句柄传递
child_process.fork() 建立 IPC :创建 pipe → NODE_CHANNEL_FD 环境变量传递 fd 编号 → 子进程 uv_pipe_open 打开 → 父子通过 process.send() 交换 JSON。
句柄传递(cluster 核心) :Primary 通过 uv_write2() + SCM_RIGHTS 将 TCP 监听 fd 发送给 Worker。内核在 Worker 的 fd 表中创建新 fd 指向同一 socket inode。这就是多 Worker 共享端口的全部秘密------没有魔法,只有 fd 继承。
6. Buffer:JS 与系统之间的数据货币
6.1 本质定位
JavaScript 原生没有字节数组。Buffer 是 Node.js 与操作系统交换数据的唯一中介:
arduino
字符串/对象 ──编码──▶ Buffer(字节序列)──write──▶ 操作系统
操作系统 ──read──▶ Buffer(字节序列)──解码──▶ 字符串/对象
Buffer 继承自 Uint8Array(通过中间类 FastBuffer),底层内存由 V8 ArrayBuffer 的 BackingStore 承载------JS 和 C++ 共享同一块物理内存,零拷贝。
6.2 内存分配策略:64KB Pool
scss
Buffer.poolSize = 64 * 1024; // 64KB 内存池
allocate(size)
├── size = 0 → new FastBuffer() 空 Buffer
├── size < 32KB → 从 Pool 切出(共享 ArrayBuffer)
│ poolOffset += size → alignPool()(8 字节对齐)
└── size ≥ 32KB → createUnsafeBuffer(size) 独立分配
关键阈值 32KB (poolSize >>> 1):小于从池中切(快、复用),大于等于独立分配(避免池碎片化)。
createUnsafeBuffer 的分层策略:
| size | 分配方式 | 初始化 |
|---|---|---|
| ≤ 64 字节 | new FastBuffer(size)(V8 堆内) |
零初始化 |
| 64B ~ 32KB | Pool 切分(共享 ArrayBuffer) | 未初始化 |
| ≥ 32KB | 独立 ArrayBuffer::NewBackingStore |
未初始化 |
6.3 alloc vs allocUnsafe
javascript
Buffer.alloc(size) // 安全:零初始化或填充指定值
Buffer.allocUnsafe(size) // 快速:走 Pool 或独立分配,不清零(可能含历史数据)
Buffer.from(string) // 字符串编码为字节(小串走 Pool,大串独立分配)
Buffer.from(arrayBuffer) // 共享同一内存的视图(零拷贝)
6.4 FastBuffer:内部高效创建
javascript
// lib/internal/buffer.js
class FastBuffer extends Uint8Array {}
// Buffer.prototype 就是 FastBuffer.prototype
// Buffer.__proto__ === Uint8Array
FastBuffer 绕开 Buffer() 构造函数的废弃警告路径,用于内部高效创建实例。作为 Symbol.species 返回值,确保 slice/filter 返回 FastBuffer 实例。
6.5 C++ 层性能优化
| 优化 | 机制 |
|---|---|
| V8 Fast API | copy/compare/writeString 跳过 FunctionCallbackInfo 参数解析,JIT 直接调用 C++ |
| simdutf | SIMD 向量化加速 UTF-8/UTF-16 编码转换,长字符串性能提升显著 |
| JS 层数值读写 | readUInt32LE 等 ~60 个方法在 JS 层实现,避免频繁 JS→C++ 边界跨越 |
| 共享 TypedArray | 浮点数读写复用全局 Float32Array/Float64Array 缓冲区做位模式转换 |
| 8 字节对齐 | alignPool() 保证内存对齐,提升 CPU 访问效率 |
| markAsUntransferable | 防止 Pool ArrayBuffer 被 Worker 间 transfer 破坏 |
6.6 Buffer 在 Node.js 中的位置
Buffer 是所有 I/O 操作的中间货币------文件读写、网络传输、子进程通信的数据都必须经过 Buffer:
scss
net.Socket 接收数据 → libuv alloc_cb 分配 Buffer → C++ 填充 → JS onread(nread, Buffer)
fs.readFile → C++ uv_fs_read → Buffer → JS callback(err, Buffer)
process.stdout.write('hello') → Buffer 编码 → C++ uv_write → 内核
7. Stream:数据流动的通用抽象
7.1 为什么有 fd 还要 Stream?
fd 是操作系统给的原材料,但它只是字节级读写接口,缺乏 5 个关键能力:
| fd 的缺陷 | Stream 的解决 |
|---|---|
| 只有字节读写,无分块概念 | chunk 分块处理,内存受控 |
| 无 JS 层缓存队列 | 内部缓冲区自动管理 |
| 无背压机制 | 读太快/写太慢时自动流量控制 |
| 文件/socket/pipe API 不统一 | Readable/Writable 一套接口 |
| 无标准事件 | end/close/error 统一生命周期 |
一句话:fd 是内核层的统一抽象,Stream 是 JS 层的统一抽象。Stream 把 fd 的裸读写包装成带背压、分块、事件驱动的标准模型。
7.2 Stream 的定位边界
Stream 是 Node.js IO 编程的基础,但不是所有概念的基础:
- 基于 Stream:http、net、fs、zlib、child_process、process.stdin/stdout/stderr
- 与 Stream 无关:事件循环、定时器、Promise、模块系统、V8、path/url/os/util
层级关系:fd(内核) → libuv(事件驱动) → C++ Binding(桥接) → Stream(JS层封装) → 高层模块
7.3 核心定义
10GB 文件不能一次读入内存。Stream 的核心思想是分块处理:
ini
传统: [10GB 一次性加载] → 处理
流式: [64KB] → 处理 → [64KB] → 处理 → ... → 结束
Stream = 带有背压控制的异步分块数据处理抽象。 解决三个问题:速度不匹配(背压)、接口不统一(统一 read/write/pipe)、生命周期复杂(construct → data → end → close)。
7.4 继承体系
arduino
EventEmitter
└── Stream
├── Readable
│ └── Duplex ← 原型链继承 Readable + 寄生混入 Writable
│ └── Transform → PassThrough
└── Writable
Duplex 寄生混入 :JS 不支持多继承。Duplex 原型链继承 Readable,Writable 的方法通过寄生方式 拷贝到 Duplex.prototype。Duplex 内部维护两套独立缓冲区(读/写),各自独立管理背压。
7.5 背压传播链
scss
消费者慢 → writable.write() 返回 false → readable 暂停 push
→ 底层 ReadStop() → 内核缓冲区积压 → TCP 滑动窗口收缩(背压传递到对端)
7.6 五层桥接体系
arduino
JS 层: Readable / Writable / Duplex / Transform .pipe() / backpressure
C++ 桥接: StreamBase → TCPWrap / PipeWrap / TTYWrap / FileHandle
libuv 层: uv_tcp_t / uv_pipe_t / uv_tty_t / uv_fs_t uv_read_start / uv_write
内核层: fd / inode / epoll / kqueue / IOCP
同一种 pipe 操作适用于完全不同的数据源------因为所有实现遵守同一套 Stream 契约:
javascript
fs.createReadStream('input.txt')
.pipe(zlib.createGzip()) // Transform
.pipe(crypto.createCipheriv(...)) // Transform
.pipe(net.connect(8080, 'server'));// Duplex
// 背压自动传播,内存始终受控
8. stdio、fd、Stream 三者关系
8.1 层次定义
arduino
stdio = fd 0/1/2 的语义约定(操作系统层,非独立技术)
│ Node.js 启动时惰性包裹
▼
process.stdin → Readable Stream(包裹 fd 0)
process.stdout → Writable Stream(包裹 fd 1)
process.stderr → Writable Stream(包裹 fd 2)
- fd:内核资源编号(最底层)
- stdio:fd 0/1/2 的 POSIX 语义约定
- Stream:JS 数据流抽象,可包裹 fd 也可完全独立(如 PassThrough)
8.2 数据读写路径
写入 process.stdout.write('hello'):
scss
JS Writable.write() → _writev() → handleWriteReq() → JS→C++ 桥接
→ StreamBase::WriteString<UTF8>() → LibuvStreamWrap::DoWrite()
→ uv_write() → 系统调用 write(1, buf, len) → 终端/管道/文件
读取 net.Socket 接收数据:
css
内核 fd 可读 → epoll 通知 → LibuvStreamWrap::OnUvRead()
→ StreamBase::CallJSOnreadMethod() → onStreamRead() → socket.push(buffer)
→ Readable 触发 'data' 事件 → 用户回调
8.3 C++ 类继承全景
arduino
AsyncWrap → HandleWrap → LibuvStreamWrap(实现 StreamBase)
├─ TCPWrap (uv_tcp_t) → net.Socket
├─ PipeWrap (uv_pipe_t) → net.Socket / IPC
└─ TTYWrap (uv_tty_t) → tty.Stream
AsyncWrap → FileHandle(实现 StreamBase,持有 int fd_)
9. 模块系统:CJS 与 ESM
| 特性 | CommonJS | ESM |
|---|---|---|
| 加载 | require() 同步 |
import 异步 |
| 缓存 | Module._cache[id] |
ModuleMap |
| 顶层 await | 不支持 | 支持 |
CJS 加载五步 :Module._load() → _resolveFilename() → _cache 检查 → new Module() → _extensions['.js']() 编译执行 → module.exports
internal modules (internalBinding、primordials)在 Bootstrap 阶段注入到每个内置模块作用域中,用户代码无法访问。
10. 贯穿案例:http.createServer().listen(8080) 全链路
scss
=== 创建阶段 ===
http.createServer(handler) [lib/http.js]
→ new Server() → net.createServer() [lib/net.js]
→ const { TCP } = internalBinding('tcp_wrap')
→ new TCP() [src/tcp_wrap.cc]
→ TCPWrap::New()
→ BaseObject(realm, jsObj) // JS↔C++ 关联
→ uv_tcp_init(loop, &handle_) // 创建 libuv handle
=== 监听阶段 ===
server.listen(8080) [lib/net.js]
→ tcp.bind() → TCPWrap::Bind() → uv_tcp_bind()
→ tcp.listen() → TCPWrap::Listen()
→ uv_tcp_listen() → uv__io_start()
→ epoll_ctl(epfd, EPOLL_CTL_ADD, fd, {EPOLLIN})
=== 连接阶段 ===
内核新连接到达 → epoll_wait() → pending_queue
→ TCPWrap::OnConnection() [C++]
→ uv_accept() + MakeCallback('onconnection') [C++ → JS]
→ new net.Socket (Duplex 流) → 'connection' 事件
→ HTTP 解析 → 'request' 事件 → handler(req, res)
11. 全景串联:概念依赖图
scss
┌─────────────┐
│ 启动引导 │ Bootstrap + RegisterBuiltinBindings
│ (Bootstrap) │ + internalBinding 闭包
└──────┬──────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌───────────┐ ┌──────────┐ ┌──────────────┐
│ 事件循环 │ │ 模块系统 │ │ Binding 机制 │
│(7阶段 + │ │(CJS/ESM) │ │(BaseObject + │
│ epoll/ │ │ │ │ InternalField)│
│ kqueue/ │ │ │ │ │
│ IOCP) │ │ │ │ │
└─────┬─────┘ └────┬─────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
┌───────────┐ ┌──────────┐ ┌──────────────┐
│ 异步 I/O │ │ Buffer │ │ 系统调用 │
│(epoll直连 │ │(64KB Pool│ │(fd 继承 / │
│ 线程池模拟)│ │ 零拷贝 │ │ SCM_RIGHTS │
│ │ │ FastAPI) │ │ 句柄传递) │
└─────┬─────┘ └────┬─────┘ └──────┬───────┘
│ │ │
└────────────┼───────────────┘
▼
┌───────────────────┐
│ Stream 抽象 │ Duplex 寄生继承 + 背压传播
│ (R/W/Duplex/ │ + highWaterMark
│ Transform) │
└──────────┬────────┘
▼ StreamBase 桥接
┌───────────────────┐
│ LibuvStreamWrap │ uv_stream_t 统一所有流式 fd
│ TCP/Pipe/TTY/File │ TCP 与 Unix Socket 共享读写逻辑
└──────────┬────────┘
▼
┌───────────────────┐
│ net / http / fs │ 用户看到的高级 API
│ child_process │
└───────────────────┘
12. 本质总结
Node.js 的设计本质是一个分层桥接系统,核心概念层层递进:
| # | 概念 | 本质 | 与其他概念的关联 |
|---|---|---|---|
| 1 | V8 引擎 | 执行 JS 的沙箱 | 沙箱无法访问系统 → 需要 Binding |
| 2 | C++ Binding | 突破沙箱的桥梁 | internalBinding() 注入 ~70 个模块到 JS |
| 3 | BaseObject | JS↔C++ 对象关联 | Internal Field 存指针,Unwrap 反向取出 |
| 4 | libuv | 跨平台异步 I/O | epoll/kqueue/IOCP 驱动事件循环 |
| 5 | 事件循环 | 调度中枢 | 七阶段轮转,所有异步回调在此排队 |
| 6 | fd | OS 统一抽象 | 管道/Socket/文件都是 fd,read/write 统一 |
| 7 | Buffer | JS↔OS 数据货币 | Pool 池分配、零拷贝、所有 I/O 的中间载体 |
| 8 | Stream | 数据流动抽象 | Duplex 寄生继承、背压控制、pipe 统一接口 |
一条线串起全部:
scss
用户写 JS
→ require() 加载模块
→ internalBinding() 获取 C++ binding
→ BaseObject 关联 JS↔C++ 对象
→ libuv 注册 handle 到事件循环
→ epoll/kqueue/IOCP 监控 fd
→ 内核处理 I/O → 事件就绪
→ Buffer 承载数据(Pool 池分配、零拷贝共享)
→ Stream push/write 触发用户回调
这就是 Node.js 的全部本质。