Part 1:Node.js 是怎么跑起来的
一、Node.js 是 JS 的服务器运行环境
这句话到底什么意思?
以前你写 JS:
-
只能在 浏览器 里跑
-
只能操作 页面
Node.js 做了一件事:
把 JavaScript 从浏览器里"解放"出来
让 JS 可以:
-
读文件
-
起 HTTP 服务
-
访问数据库
-
调系统资源
📌 类比你熟的:
- JS + Node ≈ C# + .NET Runtime
Node.js 程序是怎么启动的?
bash
node app.js
背后发生了什么(简化版):
-
Node 启动一个 进程
-
创建一个 JS 主线程
-
执行你的
.js文件 -
开始跑 事件循环
二、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 的做法是:
-
全部请求都先注册 I/O
-
JS 线程立刻返回
-
谁先好,谁先回调
📌 所以 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 代码的模板