Node.js事件循环机制详解

什么是事件循环?

事件循环是Node.js最核心的机制之一,理解它对于写出高性能的Node.js代码至关重要。简单来说,事件循环让Node.js能够处理大量并发请求,而不需要为每个请求创建新线程。

为什么需要事件循环?

在传统的服务器端语言中(如Java、PHP),每个请求通常会创建一个新线程。假设你的服务器同时有1000个请求,就需要1000个线程。但线程是昂贵的——每个线程占用约1MB内存,1000个线程就是1GB!而且线程切换也有开销。

Node.js采用完全不同的方式:单线程 + 事件循环。一个线程处理所有请求,通过事件循环来协调异步操作。

事件循环的6个阶段

事件循环有6个阶段,每个阶段都有一个先进先出的回调队列:

   ┌───────────────────────┐
┌──>        timers         │<── setTimeout/setInterval
│  └───────────┬───────────┘
│  ┌───────────┴───────────┐
│  │     pending callbacks  │<── 某些系统操作
│  └───────────┬───────────┘
│  ┌───────────┴───────────┐
│  │       idle, prepare    │<── 内部使用
│  └───────────┬───────────┘
│  ┌───────────┴───────────┐
│  │         poll          │<── I/O回调、新事件
│  └───────────┬───────────┘
│  ┌───────────┴───────────┐
│  │        check          │<── setImmediate
│  └───────────┬───────────┘
│  ┌───────────┴───────────┐
└──┤    close callbacks    │<── socket.close()
   └───────────────────────┘

1. Timers阶段

执行setTimeout()和setInterval()的回调。注意,定时器不保证精确时间,只是在指定时间后尽快执行。

setTimeout(() => {
  console.log('定时器执行了');
}, 100);

// 即使设置为100ms,实际执行时间可能更长

2. Pending Callbacks阶段

执行某些系统操作的回调,比如TCP错误。通常很少接触。

3. Poll阶段(最重要)

这是事件循环停留时间最长的阶段:

4. Check阶段

执行setImmediate()的回调:

const fs = require('fs');

fs.readFile('./file.txt', () => {
  setTimeout(() => console.log('setTimeout'), 0);
  setImmediate(() => console.log('setImmediate'));
});
// 输出顺序:setImmediate -> setTimeout(在I/O回调中)

5. Close Callbacks阶段

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

实战示例:理解执行顺序

console.log('1. 脚本开始');

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

setImmediate(() => console.log('3. setImmediate'));

process.nextTick(() => console.log('4. nextTick'));

Promise.resolve().then(() => console.log('5. Promise'));

console.log('6. 脚本结束');

// 输出:
// 1. 脚本开始
// 6. 脚本结束
// 4. nextTick
// 5. Promise
// 2. setTimeout 或 3. setImmediate(顺序不确定)

process.nextTick() vs setImmediate()

特性 process.nextTick() setImmediate()
执行时机 每个阶段之间 check阶段
优先级 最高 较低
用途 紧急任务 I/O后的任务

注意:nextTick优先级太高,滥用会阻塞事件循环!

实际应用场景

1. 处理大量并发请求

const http = require('http');

const server = http.createServer((req, res) => {
  // 不阻塞事件循环
  setTimeout(() => {
    res.end('处理完成');
  }, 100);
});

server.listen(3000);

2. 合理拆分CPU密集任务

// 错误:阻塞事件循环
function heavyCalculation(n) {
  let result = 0;
  for (let i = 0; i < n; i++) {
    result += Math.random();
  }
  return result;
}

// 正确:分批处理
async function heavyCalculationAsync(n, batchSize = 1000000) {
  let result = 0;
  let processed = 0;

  while (processed < n) {
    const end = Math.min(processed + batchSize, n);
    for (let i = processed; i < end; i++) {
      result += Math.random();
    }
    processed = end;

    // 让出控制权给事件循环
    await new Promise(resolve => setImmediate(resolve));
  }
  return result;
}

调试技巧

使用--trace-warnings查看事件循环延迟:

node --trace-warnings app.js

小结

  1. 事件循环让Node.js高效处理并发
  2. 理解6个阶段有助于写出更好的代码
  3. nextTick优先级最高,谨慎使用
  4. CPU密集任务要分批处理,避免阻塞事件循环