Node系列 · Node基础:Node.js 概述

Node.js 不是"跑在服务器上的 JavaScript",而是一套独立的运行时(Runtime)。理解它和浏览器的边界,就能解释为什么它能做高并发 IO、为什么不能做 CPU 密集型任务、为什么需要 package.json 而浏览器页面不需要。

一、Node.js 是什么

Node.js 是基于 V8 引擎 + libuv + 一组内置核心模块(fshttpnetcrypto 等)的 JavaScript 运行时,主要用于在操作系统层执行 JavaScript 代码。

Node.js 运行时

应用代码(JavaScript)

业务逻辑

第三方包(npm)

内置模块 require

V8 引擎
编译 & 执行 JS

libuv
事件循环 / 异步 IO / 线程池

内置 C++ 绑定
fs / net / crypto ...

操作系统 / 内核

组件 角色
V8 Google 的 JS 引擎,负责把 JS 编译为机器码并执行。语法、数据类型、执行模型由 ECMAScript 规范定义,V8 是规范的实现
libuv 用 C 写的事件循环库;封装了异步 IO、线程池、信号、子进程;屏蔽操作系统差异
内置模块 用 C++ 实现,绑定到 JS 层,对外暴露 require('fs') 这类 API

::: info
Node.js 不是"前端框架",也不属于任何一门后端语言。它是一个宿主环境——和浏览器宿主环境平行。浏览器宿主提供 DOM、BOM、Fetch;Node 宿主提供 fshttpprocess
:::

二、和浏览器运行时的核心差异

Node 运行时和浏览器运行时的差异,决定了它能做什么、不能做什么。这一节从 6 个核心维度对比:

维度 浏览器 Node.js
主要宿主对象 window / document global / globalThis / process
默认入口 HTML + <script> 标签 .js 文件 + package.jsonmain
内置模块 DOM、BOM、Fetch、Storage fshttpnetpathcryptostream
模块系统 ESM(<script type="module"> CommonJS(默认) + ESM(需 .mjspackage.json"type": "module"
多线程 Web Worker(受限) Worker Threads + 真正的多进程
进程控制 不能主动退出 process.exit() / process.kill() / 子进程
权限模型 沙箱(同源策略) 本机用户权限,文件 / 网络完全开放

::: warning
Node.js 拥有本机文件系统与网络权限。任何被执行的 JS 都可以读写硬盘、监听端口、调用子进程。 这一点与浏览器完全相反——不要把来源不明的 node script.js 当成"只是跑个 JS"。
:::

三、单线程与异步 IO

Node.js 主线程是单线程:JS 代码始终跑在同一个调用栈上。它能扛高并发的关键不是"CPU 多核",而是 IO 操作全部异步 + 事件循环调度

文件系统 libuv 线程池 主线程(JS) 文件系统 libuv 线程池 主线程(JS) 主线程立即继续执行 fs.readFile('a.txt', cb) fs.readFile('b.txt', cb) 读 a.txt 读 b.txt a.txt 内容 触发 a 的回调 b.txt 内容 触发 b 的回调

关键结论:

  • CPU 密集任务(同步循环、加密、压缩、JSON 序列化大对象)会阻塞整个事件循环,导致所有请求排队等待
  • IO 密集任务(文件读写、网络请求、数据库查询)天然适合 Node——内核和线程池处理,主线程只关心回调

::: tip 一句话判断该不该用 Node
“请求量高、每次请求等待外部资源(DB / 第三方 API / 文件)多、本身的计算逻辑轻”——这种场景 Node 是首选。反过来,如果业务是图像处理、视频转码、复杂数学运算,应当把重计算交给专用服务,Node 只负责编排。
:::

基于上面的架构特点,可以反过来判断"什么场景 Node 不擅长",这是下一节的主题。

四、适用与不适用场景

适合

  • Web API / BFF 层:Express、Koa、NestJS、Fastify 这类框架是 Node 最成熟的领域
  • 实时通信:WebSocket 服务(聊天、协作、白板)
  • 中间层 / 网关:聚合多个下游接口、做协议转换
  • CLI 工具:前端构建工具(Vite、webpack)、脚手架(create-vuenpm init
  • 轻量脚本:爬虫、定时任务、数据迁移

不擅长

  • CPU 重计算:图像处理、视频转码、科学计算、AI 推理。原因是主线程一旦被长任务占据,所有请求都得排队——CPU 密集场景的并发收益会被单线程结构吞掉
  • 强事务一致性的金融核心系统:这类业务依赖成熟的分布式事务框架、关系型数据库生态、运维工具链,Java 系在该领域积累更深;Node 生态的相关基础设施相对薄弱,生产案例少

::: info
“不擅长"不是"不能做”。Node 完全可以通过 Worker Threads、子进程或外部服务调用来扩展能力——只是相比 Go、Rust、Java 这些原生支持并发的语言,性价比更低。
:::

五、一次完整的 Node 执行流程

console.log('1. 同步执行开始');

setTimeout(() => {
  console.log('3. 宏任务回调(timers 阶段)');
}, 0);

Promise.resolve().then(() => {
  console.log('2. 微任务(promise)');
});

console.log('4. 同步执行结束');
$ node hello.js
1. 同步执行开始
4. 同步执行结束
2. 微任务(promise)
3. 宏任务回调(timers 阶段)

事件循环按阶段推进,每个阶段处理一类宏任务,阶段切换前清空微任务:

同步代码

清空微任务队列
nextTick + Promise.then

timers 阶段
setTimeout / setInterval

pending callbacks

idle, prepare

poll 阶段
I/O 回调 / 阻塞等待新 I/O

check 阶段
setImmediate

close callbacks

清空微任务队列

还有任务?

进程退出

每个节点的真实行为:

同步代码

JS 主线程自上而下立即执行的代码。这是整个流程唯一真正在 V8 上跑的环节。遇到 setTimeoutPromisefs.readFile 等异步调用时不会阻塞——注册回调后立即返回。

进入循环前的微任务队列

同步代码全部跑完后、事件循环进入 timers 之前,会一次性清空微任务队列。process.nextTick 的优先级高于 Promise.then——同一轮里所有 nextTick 全部跑完,才会轮到 Promise。

process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
// 输出:nextTick → promise

timers 阶段

执行 setTimeoutsetInterval 注册的回调。这里有两个隐藏约束:

  • 最小延迟 1ms:即使写 setTimeout(fn, 0),也不会在 0ms 时触发,会被规范强制抬到 1ms
  • 不保证精确:轮询到 timers 时只取"已经到期"的回调;如果 poll 阶段耗时较长,下一次 timers 触发会跟着推迟

pending callbacks

执行延迟到下一个循环迭代的 I/O 回调(例如某些系统级错误回调)。日常业务代码几乎碰不到,多数 Node 开发者可以忽略此阶段

idle, prepare

Node 内部使用的阶段,不对外暴露。libuv 在这里做一些空闲时的优化工作。用户代码不会在这里执行。

poll 阶段

事件循环最核心的阶段。它做两件事:

  1. 执行 poll 队列里已经到期的 I/O 回调(fs.readFilenet 的 data 事件等)
  2. 若所有阶段都没有更多回调(timers 队列空、check 队列空、poll 队列空),poll 会阻塞等待新 I/O

阻塞等待不是 bug,而是设计——Node 不会在无事可做时浪费 CPU。只有当这些条件之一满足时,poll 才会停止等待:

  • timers 队列有到期回调
  • setImmediate 注册了新任务
  • poll 队列本身有新回调

check 阶段

执行 setImmediate 注册的回调。这是 Node 提供的"下一轮立即执行"机制——不经过 timers 的 1ms 最小延迟。

setImmediate 经常和 setTimeout(fn, 0) 拿来比较:在 I/O 回调内部,前者总是比后者先触发,因为 poll 阶段之后紧跟 check,而 timers 要等到下一轮。

close callbacks

执行 socket.on('close', ...)process.exit() 后的清理回调等关闭事件。

阶段后的微任务清空

每个宏任务阶段执行完毕、即将进入下一阶段时,会再清空一次微任务队列。这保证了微任务总是优先于下一个宏任务阶段

循环判断

如果 timers、check、poll 三个队列都为空,事件循环就退出,进程随之结束。常见的退出场景:

  • 所有异步 I/O 完成 + 没有定时器 + 没有 setImmediate
  • 显式调用 process.exit()

这个模型会贯穿后续所有章节——理解它,就理解了为什么 setImmediate 不一定比 setTimeout(fn, 0) 快、为什么文件读取回调默认在 poll 阶段触发。

六、Node.js 版本选择

版本 状态 何时用
Current(如 24.x) 当前开发版 想用最新语法、fetch、内置测试运行器
LTS(如 22.x Iron) 长期维护,推荐 生产环境、本系列全部示例
EOL 已停止维护 升级

本文所有示例在 Node.js 22 LTS 验证通过;除明确标注外,不依赖任何实验性 API。

七、小结

  • Node.js = V8 + libuv + 内置模块,是 JS 的服务端运行时,不是后端语言
  • 单线程 + 异步 IO + 事件循环是其架构核心;CPU 密集场景需要外移
  • 拥有本机权限,不要把 node script.js 当成"沙箱里跑 JS"
  • 选择版本优先 LTS;本文使用 Node 22
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐