Day 2|Node.js 运行机制、模块系统与异步初探

Part 1:Node.js 是怎么跑起来的


一、Node.js 是 JS 的服务器运行环境

这句话到底什么意思?

以前你写 JS:

  • 只能在 浏览器 里跑

  • 只能操作 页面

Node.js 做了一件事:

把 JavaScript 从浏览器里"解放"出来

让 JS 可以:

  • 读文件

  • 起 HTTP 服务

  • 访问数据库

  • 调系统资源

📌 类比你熟的:

  • JS + Node ≈ C# + .NET Runtime

Node.js 程序是怎么启动的?

bash 复制代码
node app.js

背后发生了什么(简化版):

  1. Node 启动一个 进程

  2. 创建一个 JS 主线程

  3. 执行你的 .js 文件

  4. 开始跑 事件循环


二、JS 执行是「单线程」

什么叫单线程?

同一时间,只执行一段 JS 代码

javascript 复制代码
console.log(1)
console.log(2)

永远是:

bash 复制代码
1
2

📌 没有并行执行 JS 代码这回事。


为什么 Node 要坚持单线程?

因为这样:

  • 不用加锁

  • 不会死锁

  • 状态简单

📌 这对后端开发是巨大优势


三、I/O 是异步的(核心)

什么是 I/O?(复习一句)

程序和外部世界打交道

  • 文件

  • 网络

  • 数据库

  • 第三方接口


同步 I/O(阻塞)

javascript 复制代码
const data = fs.readFileSync('a.txt')
console.log(data)

发生的事:

  • JS 卡住

  • 啥都不能干


异步 I/O(Node 默认)

javascript 复制代码
fs.readFile('a.txt', (err, data) => {
  console.log(data)
})
console.log('next')

执行顺序:

bash 复制代码
next
文件内容

📌 重点

  • JS 不等 I/O

  • I/O 在后台干活


四、非阻塞 I/O 是怎么救并发的?

假设你有 1000 个请求

每个请求都要:

  • 查数据库

  • 读文件

  • 调接口

如果是阻塞的:

  • 等 1 个 → 后面 999 个都卡住

Node 的做法是:

  1. 全部请求都先注册 I/O

  2. JS 线程立刻返回

  3. 谁先好,谁先回调

📌 所以 Node 不怕并发请求多


五、事件驱动(Event-driven)

Node 世界里一切靠事件

你写的代码本质都是:

bash 复制代码
当 XX 发生时,做 YY

比如:

javascript 复制代码
server.on('request', () => {})
fs.readFile('a.txt', () => {})

六、Event Loop

Event Loop 是什么?

一个不停转的"调度器"

它负责:

  • 看有没有回调

  • 有就执行

  • 执行完继续转


极简流程(记这个就够)

bash 复制代码
执行同步 JS
↓
遇到异步 → 交给系统
↓
系统完成 → 回调入队
↓
Event Loop 执行回调

📌 现在不纠结细节

  • 不讲宏任务微任务

  • 不讲阶段


七、为什么 Node 不怕并发?(串起来)

你现在可以把整套逻辑串成一句话:

Node 用 单线程执行 JS

慢的 I/O 交给系统,

事件循环 处理回调,

所以能同时应付大量请求。


八、为什么 Node 适合 I/O 密集型?

I/O 密集型 = 什么?

  • API 服务

  • Agent 调 LLM

  • 数据库中转

  • 网关

👉 特点:等得多,算得少


Node 在这种场景的优势

  • JS 不阻塞

  • 内存占用小

  • 并发连接多

📌 所以:

Node = 天生的 API / Agent 引擎


九、你现在至少要"记住"的 4 个点

不用背原理,只记结论:

1️⃣ Node.js 是跑 JS 的服务器环境

2️⃣ JS 永远单线程执行

3️⃣ I/O 默认是异步非阻塞

4️⃣ 并发靠事件循环而不是多线程


🔍 30 秒自检(很重要)

你现在应该能回答:

  • 为什么 while(true) 会卡死 Node?

  • 为什么 fs.readFile 不会?

  • 为什么 Node 能同时处理很多请求?


先给结论

while(true) 会卡死 Node,
因为它一直占着 JS 主线程,
Event Loop 永远没机会工作。


一、Node 里只有一个"干活的人"

记住这一点:

Node 只有一个 JS 主线程

这个线程要负责:

  • 执行你的 JS 代码

  • 执行所有回调

  • 执行 Promise / async

📌 没有"备用 JS 线程"。


二、while(true) 在干什么?

javascript 复制代码
while (true) {
  // 什么都不干
}

它在做一件事:

永远不结束的同步计算

结果是:

  • JS 线程被 100% 占用

  • 一秒都空不出来


三、Event Loop 为什么"被饿死"?

Event Loop 的前提是:

当前 JS 执行栈清空

也就是说:

  • 同步代码执行完

  • 才会去处理回调

但 while(true) 的问题是:
  • 执行栈永远不空

  • Event Loop 永远没机会开始下一轮

📌 所以:

  • 定时器不执行

  • fs 回调不执行

  • HTTP 请求也回不了


四、对比:为什么 fs.readFile 不会卡死?

javascript 复制代码
fs.readFile('a.txt', () => {
  console.log('文件读完了')
})

console.log('我还能继续')

这里发生的是:

1️⃣ JS 发起 I/O

2️⃣ I/O 交给系统

3️⃣ JS 线程立刻空闲

4️⃣ Event Loop 可以继续转

📌 JS 没被占住


五、本质对比

场景 会不会卡死 原因
while(true) ✅ 会 同步代码占满 JS
for (大循环) ✅ 会 CPU 密集
fs.readFile ❌ 不会 异步 I/O
setTimeout ❌ 不会 交给事件循环

六、那 Node 要怎么"正确算东西"?

❌ 错误姿势

javascript 复制代码
// 在主线程死算
let sum = 0
for (let i = 0; i < 1e9; i++) {
  sum += i
}
console.log(sum)

✅ 正确姿势

  • 拆小块(setImmediate / setTimeout)

  • worker_threads

  • child_process

  • 丢给别的服务(最常见)

小例子:分批 + setImmediate

javascript 复制代码
let sum = 0
let i = 0
const MAX = 1e9
const STEP = 1e6

function calc() {
  const end = Math.min(i + STEP, MAX)

  for (; i < end; i++) {
    sum += i
  }

  if (i < MAX) {
    setImmediate(calc) // 让出主线程
  } else {
    console.log('结果:', sum)
  }
}

calc()

发生了什么?

  • 每算一小段

  • 主线程立刻空出来

  • Event Loop 能处理 I/O / 请求

📌 这是 Node 最重要的"算术姿势"

🧠 三种姿势怎么选?(直接照表)
场景 选哪种
小计算、循环 拆小块
中等 CPU 压力 worker_threads
重计算 / AI 外部服务

七、用一句话彻底吃透

你可以直接记这句:

Node 里,
所有"长时间不结束的同步代码",
都等于 while(true)。


Part 2 详细解读:Node.js 全局对象 & 进程

我们从这段代码出发:

javascript 复制代码
// test.js
console.log(__dirname)
console.log(__filename)

console.log(process.pid)
console.log(process.platform)
console.log(process.env.NODE_ENV)

一、__dirname:我现在"站在哪个目录"

它是什么?

当前 JS 文件所在的"绝对路径目录"

注意关键词:

不是命令行在哪

不是项目根目录(不一定)


举个 100% 会踩的坑

假设你项目结构:

bash 复制代码
project/
 ├─ src/
 │   └─ test.js
 └─ package.json

你在 project 目录执行:

bash 复制代码
node src/test.js

这时:

javascript 复制代码
console.log(__dirname)

输出的是:

bash 复制代码
/project/src

📌 而不是 /project


为什么这个很重要?

因为你以后写文件时:

javascript 复制代码
fs.readFile('./config.json')

👉 相对路径是"坑"

正确姿势是:

javascript 复制代码
fs.readFile(path.join(__dirname, 'config.json'))

📌 这条你以后会用到吐。


二、__filename:我这段代码是谁

它是什么?

当前 JS 文件的绝对路径 + 文件名

继续上面的例子:

javascript 复制代码
console.log(__filename)

输出:

bash 复制代码
/project/src/test.js

实际用途

1️⃣ 调试用

javascript 复制代码
console.log('当前文件:', __filename)

2️⃣ 工具 / 框架内部

  • 日志定位

  • 自动加载模块


三、process:Node 程序本身

如果说:

  • __dirname 是"位置"

  • process 是"身份"


四、process.pid:进程身份证号

javascript 复制代码
console.log(process.pid)

是什么?

当前 Node 进程在操作系统里的 ID

📌 类比 C#:

  • Process.GetCurrentProcess().Id

有什么用?

  • 排查服务是不是起了

  • 杀进程

  • 多进程管理(cluster / pm2)

bash 复制代码
kill 12345

五、process.platform:我跑在哪个系统上

javascript 复制代码
console.log(process.platform)

常见输出

系统
Windows win32
macOS darwin
Linux linux

📌 注意:

  • Windows 也是 win32(历史原因)

实际用途

javascript 复制代码
if (process.platform === 'win32') {
  console.log('Windows 特殊逻辑')
}

👉 写跨平台脚本必用。


六、process.env:环境变量(🔥 非常重要)

javascript 复制代码
console.log(process.env.NODE_ENV)

什么是环境变量?

系统或启动命令传给程序的"配置"

不是写在代码里的。


你在命令行这样启动:

macOS / Linux
bash 复制代码
NODE_ENV=production node app.js
Windows(PowerShell)
bash 复制代码
$env:NODE_ENV="production"
node app.js

Node 里就能读到:

bash 复制代码
process.env.NODE_ENV // 'production'

为什么环境变量这么重要?

因为它解决了 三大问题

1️⃣ 不同环境不同配置

2️⃣ 不能写死敏感信息

3️⃣ 部署方便


最经典的用法

javascript 复制代码
if (process.env.NODE_ENV === 'production') {
  // 生产环境
} else {
  // 开发环境
}

七、把这些"连成一句话"

你现在可以这样理解 Node 程序:

Node 程序是一个操作系统进程

它知道:

  • 自己在哪(__dirname

  • 自己是谁(__filename

  • 自己的身份(process.pid

  • 自己跑在哪个系统(process.platform

  • 自己现在是什么环境(process.env


八、给你一个"老手习惯"

以后你写 Node 文件,第一行几乎总会有:

javascript 复制代码
const path = require('path')

然后:

javascript 复制代码
path.join(__dirname, 'xxx')

📌 这是 Node 世界的"安全带"。


为什么一定要用 path?(先说结论)

因为不同操作系统,路径长得不一样

不同系统的路径分隔符

系统 路径
Windows C:\project\src\file.txt
macOS / Linux /project/src/file.txt
  • 在 mac / linux:看似没问题

  • 在 Windows:混用了 /\

👉 跨平台隐患


path 是什么?

它是什么模块?

Node 自带的 路径处理工具库

  • 不用安装

  • 专门用来处理路径

  • 不读文件、不访问磁盘

📌 它只"算字符串",但算得非常专业。


path.join() 是干什么的?

一句话定义

把多个路径片段,拼成一个"正确的完整路径"


再拆每个词是什么意思(逐词)

部分 意思
path 路径工具模块
join 拼接
__dirname 当前文件所在目录
'xxx' 子路径 / 文件名

👉 合起来就是:

"在当前文件目录下,找到 xxx"

你可以直接记这句:

Node 里,只要涉及路径,
一律 path.join + __dirname。


Part 3:fs 文件系统

🎯 目标:
会安全、正确、工程化地读写文件


一、fs 是什么?

javascript 复制代码
const fs = require('fs')

它是什么模块?

Node 内置的 文件系统模块(File System)

它能干的事包括:

  • 读文件

  • 写文件

  • 创建 / 删除文件

  • 创建 / 删除目录

  • 判断文件是否存在

📌 类比你熟的:

  • System.IO(C#)

二、fs 有三套 API(非常重要)

这是 Node 新手最容易乱的地方👇

类型 例子 是否阻塞
同步 readFileSync ✅ 阻塞
异步回调 readFile ❌ 不阻塞
Promise fs/promises ❌ 不阻塞

👉 服务器里永远用后两种


三、读文件(标准写法)

✅ 异步回调版(基础)

javascript 复制代码
const fs = require('fs')
const path = require('path')

const filePath = path.join(__dirname, 'data.txt')

fs.readFile(filePath, 'utf-8', (err, data) => {
  if (err) {
    console.error('读取失败', err)
    return
  }
  console.log('文件内容:', data)
})
每一行在干什么?
javascript 复制代码
fs.readFile(filePath, 'utf-8', callback)
参数 含义
filePath 文件的绝对路径
'utf-8' 编码(否则是 Buffer)
callback 读完后的回调
为什么要写 'utf-8'

不写的话:

javascript 复制代码
fs.readFile(filePath, (err, data) => {
  console.log(data)
})

输出的是:

bash 复制代码
<Buffer 68 65 6c 6c 6f>

📌 这是 二进制 Buffer,不是字符串。


四、写文件(标准写法)

javascript 复制代码
const fs = require('fs')
const path = require('path')

const filePath = path.join(__dirname, 'data.txt')
const content = 'hello node'

fs.writeFile(filePath, content, err => {
  if (err) {
    console.error('写入失败', err)
    return
  }
  console.log('写入成功')
})

注意事项

  • 如果文件不存在 → 自动创建

  • 如果文件存在 → 直接覆盖


五、同步 API(为什么要少用)

javascript 复制代码
const data = fs.readFileSync(filePath, 'utf-8')
console.log(data)

什么时候可以用?

  • 启动阶段

  • CLI 工具

  • 一次性脚本

什么时候不能用

  • Web 服务

  • API

  • Agent 主循环

📌 原因:

会卡死 JS 主线程


六、fs 背后发生了什么?(很重要)

当你写:

javascript 复制代码
fs.readFile(...)

Node 实际做的是:

1️⃣ JS 主线程发起请求

2️⃣ libuv 把任务交给系统 / 线程池

3️⃣ JS 线程继续跑

4️⃣ 文件读完 → 回调进队列

5️⃣ Event Loop 执行回调

📌 这就是"非阻塞 I/O"


七、常见文件操作(必会)

1️⃣ 判断文件是否存在(推荐)

javascript 复制代码
const path = require('path');
const fs = require('fs');

const filePath = path.join(__dirname, 'demo.txt');
fs.access(filePath, err => {
  if (err) {
    console.log('文件不存在')
  } else {
    console.log('文件存在')
  }
})

❌ 不推荐:

javascript 复制代码
fs.exists() // 已废弃
fs.access 是什么?

一句话定义:

用来检查"某个路径能不能被当前进程访问"

⚠️ 注意关键词:
访问权限,不是"文件内容"。

fs.access 什么时候用?

不是为了"防止失败",而是为了:

场景 是否推荐
启动时检查目录
写日志前确认目录
控制功能是否启用
防止 readFile 报错

你可以直接记这句:

fs.access 用来"判断环境状态",
不是用来"保证下一步一定成功"。


2️⃣ 创建目录(递归)

javascript 复制代码
fs.mkdir(
  path.join(__dirname, 'logs'),
  { recursive: true },
  err => {
    if (err) console.error(err)
  }
)

一句话:

在当前文件所在目录下,确保存在一个 logs 文件夹

(不存在就创建,存在也不报错)

📌 这句话里的"确保"非常关键。

mkdir 是什么?

make directory

创建文件夹

{ recursive: true }

递归创建目录 + 已存在不报错

javascript 复制代码
fs.mkdir('/a/b/c', { recursive: true }, cb)

发生的是:

1️⃣ /a 不存在 → 创建

2️⃣ /a/b 不存在 → 创建

3️⃣ /a/b/c 不存在 → 创建

4️⃣ 已存在 → 什么都不做

📌 安全、幂等


3️⃣ 追加写入

javascript 复制代码
fs.appendFile(filePath, '\nnew line', err => {
  if (err) console.error(err)
})

八、Promise 版 fs(🔥 推荐)

这是 Day 3 的桥梁

javascript 复制代码
const fs = require('fs/promises')

async function read() {
  const data = await fs.readFile(filePath, 'utf-8')
  console.log(data)
}

read()

📌 好处:

  • 没有回调地狱

  • 和 async/await 完美配合


九、工程级"安全模板"(直接抄)

javascript 复制代码
const fs = require('fs/promises')
const path = require('path')

const filePath = path.join(__dirname, 'data.txt')

async function main() {
  try {
    const data = await fs.readFile(filePath, 'utf-8')
    await fs.writeFile(
      path.join(__dirname, 'out.txt'),
      //data.toUpperCase()  可加可不加 只是一个教学用的"占位处理逻辑" 能看出"处理发生了"  不涉及业务复杂度
    )
  } catch (err) {
    console.error(err)
  }
}

main()

👉 这是你以后 80% fs 代码的模板

相关推荐
aidou13149 小时前
Visual Studio Code(VS Code)安装步骤
vscode·npm·node.js·环境变量
止观止10 小时前
告别 require!TypeScript 5.9 与 Node.js 20+ 的 ESM 互操作指南
javascript·typescript·node.js
一只专注api接口开发的技术猿11 小时前
淘宝商品详情API的流量控制与熔断机制:保障系统稳定性的后端设计
大数据·数据结构·数据库·架构·node.js
天远数科14 小时前
天远车辆过户查询API集成指南:Node.js 全栈视角下的二手车数据挖掘
大数据·数据挖掘·node.js·vim
全栈小516 小时前
【前端】win11操作系统安装完最新版本的NodeJs运行npm install报错,提示在此系统上禁止运行脚本
前端·npm·node.js
莫有杯子的龙潭峡谷1 天前
在 Windows 系统上安装 OpenClaw
人工智能·node.js·安装教程·openclaw
朝朝暮暮an2 天前
Node.js-第一天学习内容
node.js
lichenyang4532 天前
Node.js AI 开发入门 - 完整学习笔记
人工智能·学习·node.js
岁岁种桃花儿2 天前
NodeJs从入门到上天:什么是Node.js
前端·node.js