全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-15 9 分钟 ✍️ juanwangdev

Node.js 异步I/O与回调

Node.js 的非阻塞 I/O 模型是其高性能的核心,理解底层机制有助于性能优化。

I/O 模型架构

JavaScript
JavaScript 代码
      ↓
Node.js 绑定层(C++)
      ↓
libuv 事件循环
      ↓
┌─────────────────────┐
│  系统调用           │
│  ├─ epoll (Linux)   │
│  ├─ kqueue (macOS)  │
│  ├─ IOCP (Windows)  │
│  └─ 线程池          │
└─────────────────────┘

libuv 线程池

JavaScript
// 默认线程池大小:4
process.env.UV_THREADPOOL_SIZE = 8; // 设置为 8

// 线程池处理的操作:
// - fs 文件操作
// - crypto 加密操作
// - zlib 压缩操作
// - dns 解析
// - 用户自定义异步工作

线程池瓶颈

JavaScript
// 4 个线程并发执行文件操作
const fs = require('fs');

// 同时发起 8 个文件读取
for (let i = 0; i < 8; i++) {
  fs.readFile(`file${i}.txt`, (err, data) => {
    console.log(`file${i} 完成`);
  });
}

// 只有 4 个并发执行,其余排队等待
// 增加 UV_THREADPOOL_SIZE 可提高并发度

I/O 执行流程

JavaScript
fs.readFile('file.txt', (err, data) => {
  console.log('文件读取完成');
});

// 执行流程:
// 1. JavaScript 调用 fs.readFile
// 2. Node.js 绑定层创建 I/O 请求
// 3. libuv 提交到线程池
// 4. 线程执行系统调用
// 5. 完成后回调进入事件循环队列
// 6. poll 阶段执行回调

poll 阶段详解

JavaScript
// poll 阶段行为
// 1. 获取已完成的 I/O 事件
// 2. 执行对应的回调函数
// 3. 决定阻塞时间或进入下一阶段

// poll 阶段阻塞时间计算
// - 有 setImmediate:不阻塞,进入 check
// - 有到期定时器:不阻塞,进入 timers
// - 有活跃 handles:等待直到最近定时器到期
// - 无活跃 handles:可能退出循环

I/O 回调执行顺序

JavaScript
// I/O 回调在 poll 阶段执行
fs.readFile('file.txt', (err, data) => {
  console.log('I/O 回调');

  // I/O 回调中触发定时器
  setTimeout(() => console.log('timeout'), 0);
  setImmediate(() => console.log('immediate'));

  // 输出: I/O 回调 -> immediate -> timeout
  // I/O 回调在 poll 阶段,下一阶段是 check
});

同步 vs 异步 I/O

JavaScript
// 同步 I/O(阻塞)
const data = fs.readFileSync('file.txt');
// 线程阻塞,等待操作完成

// 异步 I/O(非阻塞)
fs.readFile('file.txt', (err, data) => {});
// 主线程继续,回调稍后执行
方式线程行为适用场景
同步阻塞等待启动时加载配置
异步不阻塞大量并发请求

Error-First 回调规范

JavaScript
// Node.js 回调规范:第一个参数是错误
fs.readFile('file.txt', (err, data) => {
  if (err) {
    // 处理错误
    console.error(err);
    return;
  }
  // 处理数据
  console.log(data);
});

// 自定义异步函数遵循规范
function asyncOperation(callback) {
  performTask((result, error) => {
    if (error) {
      callback(error);
    } else {
      callback(null, result);
    }
  });
}

I/O 性能优化

批量操作

JavaScript
// ❌ 逐个读取
for (const file of files) {
  fs.readFile(file, callback);
}

// ✅ 并发读取
const promises = files.map(f =>
  fs.promises.readFile(f)
);
await Promise.all(promises);

流式处理

JavaScript
// ❌ 一次性读取大文件
fs.readFile('large.txt', callback);

// ✅ 流式处理
const stream = fs.createReadStream('large.txt');
stream.on('data', chunk => process(chunk));
stream.on('end', () => console.log('完成'));

缓存策略

JavaScript
// 缓存读取结果
const cache = new Map();

function cachedRead(path) {
  if (cache.has(path)) {
    return Promise.resolve(cache.get(path));
  }
  return fs.promises.readFile(path).then(data => {
    cache.set(path, data);
    return data;
  });
}

监控 I/O 性能

JavaScript
// 使用 async_hooks 监控
const asyncHooks = require('async_hooks');

const hook = asyncHooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    if (type === 'FSREQCALLBACK') {
      console.log('I/O 发起:', asyncId);
    }
  },
  destroy(asyncId) {
    console.log('I/O 完成:', asyncId);
  }
});
hook.enable();

非线程池 I/O

text
// 网络 I/O 不使用线程池
// 直接使用系统事件机制(epoll/kqueue/IOCP)

const net = require('net');
const server = net.createServer();

server.listen(3000);
// 使用系统原生异步机制,不占用线程池

要点总结

  • libuv 线程池默认 4 线程,处理 fs/crypto/zlib
  • UV_THREADPOOL_SIZE 可调整线程池大小
  • I/O 回调在 poll 阶段执行
  • I/O 回调中 setImmediate 优先于 setTimeout
  • 网络操作不使用线程池,使用系统原生机制
  • 流式处理大文件,避免一次性读取

📝 发现内容有误?点击此处直接编辑

← 上一篇 Node.js 并发模型与线程池
下一篇 → Node.js 微任务与宏任务
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库