Skip to content
On this page

Node.js 事件循环

事件循环是Node.js实现异步非阻塞I/O的核心机制,理解事件循环的工作原理对于编写高效的Node.js应用程序至关重要。

事件循环概述

Node.js基于单线程事件循环模型来处理并发操作。事件循环是一个持续运行的循环,它不断检查是否有待处理的任务并执行它们。

事件循环的基本概念

javascript
// 事件循环示例
console.log('1. 同步代码开始');

setTimeout(() => {
  console.log('2. setTimeout 回调');
}, 0);

setImmediate(() => {
  console.log('3. setImmediate 回调');
});

Promise.resolve().then(() => {
  console.log('4. Promise 回调');
});

console.log('5. 同步代码结束');

// 输出顺序:
// 1. 同步代码开始
// 5. 同步代码结束
// 4. Promise 回调
// 2. setTimeout 回调
// 3. setImmediate 回调

事件循环的阶段

事件循环分为多个阶段,每个阶段都有特定的任务队列:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤   connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

1. 定时器阶段 (Timers)

这个阶段执行由setTimeout()和setInterval()设置的回调。

javascript
console.log('开始');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

console.log('结束');

// 输出:
// 开始
// 结束
// setTimeout
// setImmediate
// (顺序可能因系统负载而变化)

2. 待定回调 (Pending callbacks)

执行一些系统操作的回调,如TCP错误。

3. 轮询阶段 (Poll)

这个阶段主要执行I/O回调,包括:

  • 执行I/O相关的回调(除了定时器、setImmediate和一些关闭回调)
  • 检查定时器是否到期
javascript
const fs = require('fs');

// 这个I/O操作的回调会在poll阶段执行
fs.readFile('./file.txt', (err, data) => {
  console.log('文件读取完成');
});

4. 检测阶段 (Check)

执行setImmediate()设置的回调。

javascript
const fs = require('fs');

// 在poll阶段完成后立即执行
setImmediate(() => {
  console.log('setImmediate回调');
});

// 读取文件,其回调在poll阶段执行
fs.readFile('./file.txt', () => {
  console.log('文件读取回调');
});

5. 关闭回调 (Close callbacks)

执行关闭请求的回调,如socket.on('close')。

javascript
const net = require('net');

const server = net.createServer((socket) => {
  socket.end();
  socket.on('close', () => {
    console.log('连接关闭');
  });
});

任务队列类型

Node.js中有多种任务队列:

1. 微任务队列 (Microtask Queue)

  • Promise回调
  • process.nextTick回调
javascript
console.log('1. 同步代码');

Promise.resolve().then(() => {
  console.log('2. Promise回调');
});

process.nextTick(() => {
  console.log('3. nextTick回调');
});

console.log('4. 同步代码结束');

// 输出:
// 1. 同步代码
// 4. 同步代码结束
// 3. nextTick回调
// 2. Promise回调

2. 宏任务队列 (Macrotask Queue)

  • setTimeout回调
  • setInterval回调
  • setImmediate回调
  • I/O回调

process.nextTick

process.nextTick()不是事件循环的一部分,但与事件循环密切相关。它会在当前操作完成后立即执行回调。

javascript
console.log('1. 开始');

setImmediate(() => {
  console.log('2. setImmediate');
});

process.nextTick(() => {
  console.log('3. nextTick');
});

Promise.resolve().then(() => {
  console.log('4. Promise');
});

console.log('5. 结束');

// 输出:
// 1. 开始
// 5. 结束
// 3. nextTick
// 4. Promise
// 2. setImmediate

实际应用示例

正确使用事件循环

javascript
// 避免长时间运行的同步操作
function badExample() {
  // 这会阻塞事件循环
  for (let i = 0; i < 1000000000; i++) {
    // 执行大量计算
  }
}

// 好的做法 - 分批处理
function goodExample() {
  let i = 0;
  const batchSize = 1000;
  
  function processBatch() {
    const end = Math.min(i + batchSize, 1000000000);
    
    for (; i < end; i++) {
      // 执行计算
    }
    
    if (i < 1000000000) {
      // 让出控制权给其他任务
      setImmediate(processBatch);
    }
  }
  
  processBatch();
}

事件循环在I/O操作中的应用

javascript
const fs = require('fs');
const crypto = require('crypto');

// I/O操作不会阻塞事件循环
function ioExample() {
  console.log('开始读取文件');
  
  // 文件读取操作,不会阻塞事件循环
  fs.readFile('./large-file.txt', (err, data) => {
    if (err) throw err;
    console.log('文件读取完成');
  });
  
  console.log('继续执行其他代码');
  
  // CPU密集型操作,会阻塞事件循环
  const hash = crypto.createHash('sha256');
  hash.update('some data');
  console.log('哈希计算完成');
}

ioExample();

setImmediate vs setTimeout

javascript
// setImmediate vs setTimeout(0) 的区别
setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

// 在I/O循环中,setImmediate总是优先于setTimeout
const fs = require('fs');
fs.readFile('./file.txt', () => {
  setTimeout(() => {
    console.log('setTimeout in I/O');
  }, 0);
  
  setImmediate(() => {
    console.log('setImmediate in I/O');
  });
});

// 输出:
// setImmediate in I/O
// setTimeout in I/O

事件循环监控

javascript
// 监控事件循环延迟
function monitorEventLoop() {
  const start = Date.now();
  
  setTimeout(() => {
    const delay = Date.now() - start;
    console.log(`事件循环延迟: ${delay}ms`);
    
    // 如果延迟过大,可能表示事件循环被阻塞
    if (delay > 10) {
      console.warn('事件循环被阻塞');
    }
    
    // 继续监控
    setImmediate(monitorEventLoop);
  }, 1);
}

// 启动监控
monitorEventLoop();

性能优化建议

1. 避免长时间运行的同步操作

javascript
// 不好的做法
function syncHeavyOperation() {
  // 执行长时间同步操作
  for (let i = 0; i < 1000000000; i++) {
    // 大量计算
  }
}

// 好的做法 - 使用异步分批处理
function asyncHeavyOperation() {
  return new Promise((resolve) => {
    let i = 0;
    const total = 1000000000;
    const batchSize = 1000000;
    
    function processBatch() {
      const end = Math.min(i + batchSize, total);
      for (; i < end; i++) {
        // 处理一批数据
      }
      
      if (i < total) {
        setImmediate(processBatch);
      } else {
        resolve('完成');
      }
    }
    
    processBatch();
  });
}

2. 合理使用微任务和宏任务

javascript
// 在某些场景下,使用nextTick来确保操作在当前操作后立即执行
function ensureOrder() {
  console.log('A');
  
  process.nextTick(() => {
    console.log('C');
  });
  
  Promise.resolve().then(() => {
    console.log('D');
  });
  
  console.log('B');
  
  // 输出: A, B, C, D
}

3. 优化I/O操作

javascript
// 使用流来处理大文件,避免一次性加载到内存
const fs = require('fs');
const { Transform } = require('stream');

function processLargeFile() {
  const readStream = fs.createReadStream('./large-file.txt');
  const writeStream = fs.createWriteStream('./output.txt');
  
  const transformStream = new Transform({
    transform(chunk, encoding, callback) {
      // 异步处理每个数据块
      const processed = chunk.toString().toUpperCase();
      callback(null, processed);
    }
  });
  
  readStream
    .pipe(transformStream)
    .pipe(writeStream)
    .on('finish', () => {
      console.log('文件处理完成');
    });
}

理解事件循环的工作原理对于编写高性能的Node.js应用程序至关重要,它帮助开发者避免阻塞操作,合理安排任务执行顺序。