第 1 章:Node.js 概述与运行机制
作为后端开发者,理解 Node.js 最好的方式,是先搞懂它「单线程却高效」的核心机制。这一章从运行原理讲起,为后面所有内容打地基。
1.1 Node.js 是什么
Node.js 不是一门语言,也不是一个框架,而是一个 JavaScript 运行时——它把 Chrome 浏览器的 V8 引擎拿出来,加上了操作文件、网络、进程的能力,让 JavaScript 能在服务器端跑起来。
┌─────────────────────────────────────────────┐
│ 你的 JavaScript 代码 │
└──────────────────────┬──────────────────────┘
│
┌──────────────────────▼──────────────────────┐
│ Node.js 运行时 │
│ ┌────────────┐ ┌────────────┐ ┌────────┐ │
│ │ V8 引擎 │ │ libuv │ │ 内置模块 │ │
│ │ (执行 JS) │ │(事件循环/异步)│ │ fs/http │ │
│ └────────────┘ └────────────┘ └────────┘ │
└──────────────────────┬──────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
文件系统 网络 进程/线程池核心定位:Node.js 用 JavaScript 写后端,主打「高并发 I/O 密集」场景——API 网关、实时通信、代理转发、AI 服务编排这类「大量等待、少量计算」的任务,正是它的主场。
1.2 单线程,为什么还高效?
这是后端开发者最容易困惑的点。Java 一个请求一个线程,线程多了开销大;Node 只有一个主线程,却能扛住上万个并发连接。差别在于处理 I/O 的方式:
【传统多线程模型】一个连接 = 一个线程,线程阻塞等待 I/O
请求1 ──► 线程1 ──阻塞等待DB──► 返回
请求2 ──► 线程2 ──阻塞等待DB──► 返回
请求3 ──► 线程3 ──阻塞等待DB──► 返回
(线程切换开销大,1000 连接 = 1000 线程)
【Node 单线程异步模型】一个线程,I/O 交给系统,不阻塞
请求1 ─┐
请求2 ─┼──► 主线程(轻量处理)──► 发起异步 I/O ──► 系统线程池
请求3 ─┘ │
┌───────────────────────┘
▼ I/O 完成,回调排队
事件循环逐个执行回调 ──► 返回响应关键结论:
- Node 的「单线程」指的是执行你 JS 代码的那个主线程只有一个;
- 耗时的 I/O(读文件、查数据库、发网络请求)交给 libuv 的线程池 / 操作系统异步去做,主线程不傻等;
- I/O 完成后,回调函数进入队列,由事件循环逐个调用。
所以 Node 擅长「I/O 密集」:大量等待、少量 CPU 计算。反过来,CPU 密集(大量循环计算)会卡住唯一的主线程,那是 Node 的短板。
1.3 V8 引擎与 JIT 编译
V8 负责把 JavaScript 源码变成机器码执行。它不预先编译,而是运行时即时编译(JIT),并在运行中根据热点不断优化:
JavaScript 源码
│ 解析(Parser)
▼
抽象语法树 AST
│ 解释器 Ignition 生成字节码
▼
字节码 Bytecode ──执行──► 结果
│ 热点代码检测(反复执行的热函数)
▼
优化编译器 TurboFan ──► 高度优化的机器码不用深究 V8 内部,只需知道:频繁执行的代码会被 JIT 优化得接近编译语言速度,这是 Node 高性能的来源之一。
1.4 事件循环 Event Loop(核心)
事件循环是 Node 的「心脏」,理解它才能写出正确的异步代码。一句话概括:
事件循环不断从任务队列里取出回调执行,直到队列清空。
Node 的事件循环分几个阶段(phase),每个阶段处理一类回调:
┌───────────────────────────┐
│ timers 阶段 │ setTimeout / setInterval 到期回调
└───────────┬───────────────┘
▼
┌───────────────────────────┐
│ pending callbacks │ 上轮遗留的 I/O 回调
└───────────┬───────────────┘
▼
┌───────────────────────────┐
│ idle / prepare │ 内部用,一般忽略
└───────────┬───────────────┘
▼
┌───────────────────────────┐
│ poll 阶段 │ ← 核心:执行 I/O 回调,无任务则等待
└───────────┬───────────────┘
▼
┌───────────────────────────┐
│ check 阶段 │ setImmediate 回调
└───────────┬───────────────┘
▼
┌───────────────────────────┐
│ close callbacks │ socket 关闭等收尾回调
└───────────┬───────────────┘
▼
(一轮结束,回到 timers 开始下一轮)最重要的两个阶段:
- timers:执行到期的
setTimeout/setInterval; - poll:执行 I/O 回调(如文件读取完成、网络响应),如果没有待办任务,会在这里阻塞等待新事件。
1.5 微任务:process.nextTick 与 Promise
事件循环里还有一条「插队」规则——微任务(microtask)。Promise 的 .then 回调、process.nextTick 都是微任务,它们在当前阶段结束、下一阶段开始前优先执行:
执行栈中一个回调执行完
│
▼
┌─────────────────┐
│ 清空 nextTick 队列 │ ← process.nextTick(优先级最高)
└────────┬────────┘
▼
┌─────────────────┐
│ 清空微任务队列 │ ← Promise.then / queueMicrotask
└────────┬────────┘
▼
回到事件循环下一个阶段一个经典面试题,验证你是否理解:
console.log('1'); // 同步,先打印
setTimeout(() => console.log('2'), 0); // 宏任务,进 timers 队列
Promise.resolve()
.then(() => console.log('3')); // 微任务,优先于宏任务
process.nextTick(() => console.log('4')); // nextTick,比 Promise 更优先
// 输出顺序:1 → 4 → 3 → 2记忆口诀:同步先行,nextTick 插队最前,Promise 微任务其次,setTimeout 宏任务最后。
1.6 本章小结
| 要点 | 说明 |
|---|---|
| Node 是什么 | 一个 JavaScript 运行时(V8 + libuv + 内置模块) |
| 单线程异步 | 主线程只有一个,I/O 交给系统异步处理,靠事件循环调度 |
| 擅长场景 | I/O 密集(API 网关、代理、实时、AI 编排) |
| 短板 | CPU 密集计算会阻塞主线程 |
| 事件循环 | 分阶段循环执行回调,核心是 timers 和 poll |
| 微任务 | nextTick > Promise > 宏任务,掌握执行顺序是异步基础 |
下一章预告
理解了运行机制,接下来看代码怎么组织——CommonJS 和 ESM 两种模块规范有什么区别?npm 怎么管理依赖?