Node.js 异步编程概述
Node.js 异步模型的特点
Node.js 采用单线程、非阻塞 I/O、事件驱动的异步模型,这是其高性能的核心所在。
单线程的优势
javascript
// ✅ Node.js 单线程模型
// 主线程不会被 I/O 操作阻塞
const fs = require('fs');
console.log('开始读取文件');
fs.readFile('./data.txt', 'utf8', (err, data) => {
console.log('文件读取完成');
});
console.log('继续执行其他代码');
// 输出顺序:
// 开始读取文件
// 继续执行其他代码
// 文件读取完成优势:
- 无需考虑多线程的锁、竞态条件等问题
- 内存占用更低
- 上下文切换开销小
- 适合 I/O 密集型应用
局限性:
- CPU 密集型任务会阻塞事件循环
- 需要合理拆分任务或使用 Worker Threads
非阻塞 I/O
javascript
// ❌ 阻塞式 I/O(传统同步方式)
const data = fs.readFileSync('./data.txt', 'utf8');
console.log('文件内容:', data);
console.log('这行代码要等文件读取完才执行');
// ✅ 非阻塞式 I/O(Node.js 推荐)
fs.readFile('./data.txt', 'utf8', (err, data) => {
console.log('文件内容:', data);
});
console.log('这行代码立即执行,不用等文件读取');事件驱动
javascript
const EventEmitter = require('events');
// 创建事件发射器
const emitter = new EventEmitter();
// 注册事件监听器
emitter.on('data', (data) => {
console.log('收到数据:', data);
});
// 触发事件
emitter.emit('data', { message: 'Hello Node.js' });Node.js vs 浏览器环境
主要差异对比
| 特性 | Node.js | 浏览器 |
|---|---|---|
| 运行环境 | 服务器端 | 客户端 |
| 全局对象 | global | window |
| 模块系统 | CommonJS / ESM | ESM |
| 文件系统 | 完整访问 | 受限(需用户选择) |
| 网络权限 | 无限制 | 受 CORS 等限制 |
| 定时器精度 | 毫秒级 | 可能被节流 |
| 微任务 API | process.nextTick | 无 |
| 立即执行 | setImmediate | 无(可用 setTimeout(0)) |
事件循环差异
javascript
// Node.js 特有:process.nextTick
process.nextTick(() => {
console.log('nextTick 回调');
});
// Node.js 特有:setImmediate
setImmediate(() => {
console.log('setImmediate 回调');
});
// 浏览器和 Node.js 都有:setTimeout
setTimeout(() => {
console.log('setTimeout 回调');
}, 0);执行顺序(Node.js):
code
nextTick → Promise 微任务 → setImmediate → setTimeout(0)全局对象差异
javascript
// Node.js
console.log(global); // 全局对象
console.log(process); // 进程对象
console.log(__dirname); // 当前目录
console.log(__filename); // 当前文件路径
// 浏览器
console.log(window); // 全局对象
console.log(document); // DOM 文档
console.log(navigator); // 浏览器信息
console.log(location); // URL 信息libuv 简介
libuv 是 Node.js 的底层异步 I/O 库,负责实现事件循环和跨平台异步操作。
libuv 的核心功能
code
┌─────────────────────────────────────────────────────┐
│ Node.js 应用 │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────┐
│ V8 JavaScript 引擎 │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────┐
│ libuv │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 事件循环 │ │ 线程池 │ │
│ │ (Event Loop)│ │(Thread Pool) │ │
│ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 文件系统 │ │ 网络 I/O │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────┘线程池
libuv 默认提供 4 个线程用于处理阻塞式操作:
javascript
// 查看线程池大小
console.log(process.env.UV_THREADPOOL_SIZE); // 默认 4
// 设置线程池大小(启动前设置)
process.env.UV_THREADPOOL_SIZE = 8;
// 线程池处理的操作:
// - 文件系统操作(部分)
// - DNS 查询
// - zlib 压缩/解压
// - crypto 加密操作网络 I/O 不使用线程池
javascript
// 网络 I/O 由操作系统内核处理(epoll/kqueue/IOCP)
// 不占用线程池资源
const http = require('http');
const server = http.createServer((req, res) => {
res.end('Hello');
});
// 可以轻松支持数万并发连接
server.listen(3000);异步编程的演进
阶段一:回调函数
javascript
// 回调地狱示例
fs.readFile('file1.txt', 'utf8', (err, data1) => {
fs.readFile('file2.txt', 'utf8', (err, data2) => {
fs.readFile('file3.txt', 'utf8', (err, data3) => {
console.log(data1, data2, data3);
});
});
});阶段二:Promise
javascript
// 使用 Promise 改进
const fs = require('fs').promises;
fs.readFile('file1.txt', 'utf8')
.then(data1 => fs.readFile('file2.txt', 'utf8'))
.then(data2 => fs.readFile('file3.txt', 'utf8'))
.then(data3 => console.log('完成'))
.catch(err => console.error(err));阶段三:async/await
javascript
// 使用 async/await 最优雅
const fs = require('fs').promises;
async function readFiles() {
try {
const data1 = await fs.readFile('file1.txt', 'utf8');
const data2 = await fs.readFile('file2.txt', 'utf8');
const data3 = await fs.readFile('file3.txt', 'utf8');
console.log(data1, data2, data3);
} catch (err) {
console.error(err);
}
}Node.js 特有的异步 API
process.nextTick
javascript
// process.nextTick 优先级最高
// 在当前操作完成后立即执行,早于 Promise 微任务
process.nextTick(() => {
console.log('1. nextTick');
});
Promise.resolve().then(() => {
console.log('2. Promise');
});
console.log('0. 同步代码');
// 输出顺序:
// 0. 同步代码
// 1. nextTick
// 2. PromisesetImmediate
javascript
// setImmediate 在下一个事件循环周期执行
// 类似于 setTimeout(fn, 0),但效率更高
setImmediate(() => {
console.log('setImmediate');
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
// 在 I/O 回调中,setImmediate 总是先于 setTimeout 执行
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
});
// 输出:setImmediate → setTimeoututil.promisify
javascript
// 将回调风格的函数转换为 Promise
const util = require('util');
const fs = require('fs');
// 传统回调方式
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
// 使用 util.promisify 转换
const readFile = util.promisify(fs.readFile);
readFile('data.txt', 'utf8')
.then(data => console.log(data))
.catch(err => console.error(err));
// 或使用 fs.promises(推荐)
const fsPromises = require('fs').promises;
const data = await fsPromises.readFile('data.txt', 'utf8');最佳实践
1. 优先使用 Promise API
javascript
// ❌ 避免使用回调风格
fs.readFile('data.txt', (err, data) => { /* ... */ });
// ✅ 使用 Promise API
const fs = require('fs').promises;
const data = await fs.readFile('data.txt', 'utf8');2. 正确处理错误
javascript
// ❌ 未处理 Promise 拒绝
async function bad() {
const data = await fs.readFile('not-exist.txt', 'utf8');
}
// ✅ 使用 try-catch
async function good() {
try {
const data = await fs.readFile('data.txt', 'utf8');
return data;
} catch (err) {
console.error('读取失败:', err.message);
throw err; // 或返回默认值
}
}
// ✅ 或使用 .catch()
async function good2() {
const data = await fs.readFile('data.txt', 'utf8')
.catch(err => {
console.error('读取失败:', err.message);
return null; // 返回默认值
});
return data;
}3. 避免阻塞事件循环
javascript
// ❌ CPU 密集型任务阻塞事件循环
function heavyComputation(n) {
let result = 0;
for (let i = 0; i < n; i++) {
result += Math.sqrt(i);
}
return result;
}
// ✅ 使用 Worker Threads
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', result => console.log(result));
worker.postMessage({ n: 10000000 });
} else {
parentPort.on('message', ({ n }) => {
// 执行计算
const result = heavyComputation(n);
parentPort.postMessage(result);
});
}
// ✅ 或使用 setImmediate 分块处理
function processInChunks(data, chunkSize = 1000) {
let i = 0;
function processChunk() {
const end = Math.min(i + chunkSize, data.length);
while (i < end) {
// 处理数据
i++;
}
if (i < data.length) {
setImmediate(processChunk);
}
}
processChunk();
}4. 并发控制
javascript
// ❌ 无限制的并发
const files = ['file1.txt', 'file2.txt', /* ...100个文件 */];
await Promise.all(files.map(f => fs.readFile(f)));
// ✅ 限制并发数量
async function limitConcurrency(tasks, limit = 5) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const promise = task().then(result => {
executing.delete(promise);
return result;
});
executing.add(promise);
results.push(promise);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}性能优化建议
1. 合理使用缓存
javascript
const cache = new Map();
async function getCachedData(key) {
if (cache.has(key)) {
return cache.get(key);
}
const data = await fs.readFile(key, 'utf8');
cache.set(key, data);
return data;
}2. 使用流处理大数据
javascript
// ❌ 一次性加载大文件
const data = await fs.readFile('large.txt');
// ✅ 使用流处理
const readStream = fs.createReadStream('large.txt', { highWaterMark: 64 * 1024 });
readStream.on('data', (chunk) => {
// 处理数据块
});3. 批量操作
javascript
// ❌ 多次独立写入
for (const data of dataArray) {
await fs.writeFile('output.txt', data, { flag: 'a' });
}
// ✅ 批量写入
const allData = dataArray.join('\n');
await fs.writeFile('output.txt', allData);小结
- Node.js 的异步模型基于单线程、非阻塞 I/O 和事件驱动
- libuv 是实现异步 I/O 的核心库,提供线程池和事件循环
process.nextTick和setImmediate是 Node.js 特有的异步 API- 优先使用 Promise API(如
fs.promises)而非回调风格 - 避免阻塞事件循环,CPU 密集型任务应使用 Worker Threads
- 合理控制并发数量,避免资源耗尽
Node.js 22+ 异步编程新特性
原生 fetch 异步请求
Node.js 22+ 内置全局 fetch,基于 Promise 的 HTTP 请求:
javascript
// 无需安装 node-fetch 或 axios
const response = await fetch('https://api.example.com/data')
const data = await response.json()
// 流式响应处理
const response = await fetch('https://example.com/large-file')
for await (const chunk of response.body) {
process.stdout.write(chunk)
}AsyncLocalStorage 增强
Node.js 22+ 的 AsyncLocalStorage 更加稳定,用于在异步调用链中传递上下文:
javascript
import { AsyncLocalStorage } from 'node:async_hooks'
const requestContext = new AsyncLocalStorage()
// 在 HTTP 中间件中设置上下文
server.on('request', (req, res) => {
requestContext.run({ requestId: crypto.randomUUID(), userId: req.userId }, () => {
handleRequest(req, res)
})
})
// 在任何异步函数中获取上下文
async function logWithContext(message) {
const ctx = requestContext.getStore()
console.log(`[${ctx.requestId}] [user:${ctx.userId}] ${message}`)
}AbortController 与 AbortSignal
Node.js 22+ 完整支持 AbortController,用于取消异步操作:
javascript
const controller = new AbortController()
const { signal } = controller
// 取消 fetch 请求
setTimeout(() => controller.abort(), 5000)
try {
const response = await fetch('https://api.example.com/slow', { signal })
} catch (err) {
if (err.name === 'AbortError') {
console.log('请求已取消')
}
}
// 取消文件读取
import { readFile } from 'node:fs/promises'
try {
const data = await readFile('./large-file.txt', { signal })
} catch (err) {
if (err.name === 'AbortError') {
console.log('文件读取已取消')
}
}