{T}

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浏览器
运行环境服务器端客户端
全局对象globalwindow
模块系统CommonJS / ESMESM
文件系统完整访问受限(需用户选择)
网络权限无限制受 CORS 等限制
定时器精度毫秒级可能被节流
微任务 APIprocess.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. Promise

setImmediate

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 → setTimeout

util.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.nextTicksetImmediate 是 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('文件读取已取消')
  }
}