Skip to content

Node.js 事件循环深入解析

事件循环是 Node.js 的核心机制,它使得 Node.js 能够以非阻塞的方式处理大量并发请求。理解事件循环的工作原理对于编写高性能的 Node.js 应用至关重要。

一、事件循环基础概念

1. 什么是事件循环?

事件循环是 Node.js 处理非阻塞 I/O 操作的核心机制。由于 JavaScript 是单线程的,事件循环允许 Node.js 执行非阻塞操作,尽管事实上 JavaScript 仍然是单线程的。

javascript
// 单线程示例
console.log('1');

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

console.log('3');

// 输出顺序:
// 1
// 3
// 2

2. 事件循环与多线程的区别

javascript
// 多线程示例(伪代码)
// Thread 1
console.log('Task 1');
performBlockingOperation(); // 阻塞线程
console.log('Task 2');

// Thread 2
console.log('Task A');
performBlockingOperation(); // 阻塞线程
console.log('Task B');

// Node.js 事件循环
console.log('Task 1');
setTimeout(() => {
  console.log('Task 2');
}, 0);

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

console.log('Task 3');

// 输出顺序:
// Task 1
// Task A
// Task 3
// Task 2
// Task B

二、事件循环的六个阶段

Node.js 的事件循环由 libuv 实现,分为六个阶段,每个阶段都有特定的任务类型:

1. timers 阶段

执行 setTimeout()setInterval() 回调。

javascript
// timers 阶段示例
const fs = require('fs');

function testTimers() {
  console.log('开始执行');
  
  setTimeout(() => {
    console.log('setTimeout 1');
  }, 0);
  
  setImmediate(() => {
    console.log('setImmediate 1');
  });
  
  setTimeout(() => {
    console.log('setTimeout 2');
  }, 0);
  
  process.nextTick(() => {
    console.log('nextTick 1');
  });
  
  console.log('执行结束');
}

testTimers();

// 输出顺序:
// 开始执行
// 执行结束
// nextTick 1
// setTimeout 1
// setTimeout 2
// setImmediate 1

2. pending callbacks 阶段

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

3. idle, prepare 阶段

仅 Node.js 内部使用。

4. poll 阶段

检索新的 I/O 事件,执行与 I/O 相关的回调(除了关闭回调、定时器回调和 setImmediate())。

javascript
// poll 阶段示例
const fs = require('fs');

console.log('开始执行');

// I/O 操作会进入 poll 阶段
fs.readFile(__filename, () => {
  console.log('文件读取完成');
  
  setTimeout(() => {
    console.log('setTimeout in I/O callback');
  }, 0);
  
  setImmediate(() => {
    console.log('setImmediate in I/O callback');
  });
});

console.log('执行结束');

// 输出顺序:
// 开始执行
// 执行结束
// 文件读取完成
// setImmediate in I/O callback
// setTimeout in I/O callback

5. check 阶段

执行 setImmediate() 回调。

6. close callbacks 阶段

执行关闭事件的回调,如 socket.on('close', ...)

三、微任务与宏任务

Node.js 中有两种类型的任务队列:

1. 微任务(Microtasks)

包括 process.nextTick() 和 Promise 回调。

javascript
// 微任务示例
console.log('1');

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

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

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

console.log('5');

// 输出顺序:
// 1
// 5
// 4
// 3
// 2

2. 宏任务(Macrotasks)

包括 setTimeoutsetIntervalsetImmediate、I/O 操作等。

javascript
// 宏任务与微任务的交互
setTimeout(() => {
  console.log('setTimeout 1');
  
  Promise.resolve().then(() => {
    console.log('Promise in setTimeout');
  });
  
  process.nextTick(() => {
    console.log('nextTick in setTimeout');
  });
}, 0);

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

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

// 输出顺序:
// nextTick 1
// Promise 1
// setTimeout 1
// nextTick in setTimeout
// Promise in setTimeout

四、process.nextTick() 深入解析

process.nextTick() 是 Node.js 特有的 API,它会在事件循环的下一个滴答执行。

1. nextTick 的执行时机

javascript
// nextTick 执行时机示例
console.log('1');

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

console.log('2');

// 输出顺序:
// 1
// 2
// nextTick 1

2. nextTick 与 Promise 的区别

javascript
// nextTick 与 Promise 的执行顺序
console.log('start');

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

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

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

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

console.log('end');

// 输出顺序:
// start
// end
// nextTick 1
// nextTick 2
// Promise 1
// Promise 2

3. nextTick 的递归调用问题

javascript
// 危险的递归 nextTick
let count = 0;

function recursiveNextTick() {
  console.log(`nextTick ${++count}`);
  
  if (count < 10) {
    process.nextTick(recursiveNextTick);
  }
}

process.nextTick(recursiveNextTick);

console.log('主程序结束');

// 输出:
// 主程序结束
// nextTick 1
// nextTick 2
// nextTick 3
// ... (直到 count = 10)
// 注意:这会阻塞事件循环!

五、setImmediate() 与 setTimeout() 的区别

1. 执行顺序

javascript
// setImmediate 与 setTimeout 的执行顺序
console.log('开始');

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

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

console.log('结束');

// 输出顺序(可能变化):
// 开始
// 结束
// setTimeout
// setImmediate
//
// 或者:
// 开始
// 结束
// setImmediate
// setTimeout

2. 在 I/O 回调中的执行顺序

javascript
// 在 I/O 回调中 setImmediate 与 setTimeout 的执行顺序
const fs = require('fs');

fs.readFile(__filename, () => {
  console.log('I/O 回调开始');
  
  setTimeout(() => {
    console.log('setTimeout in I/O');
  }, 0);
  
  setImmediate(() => {
    console.log('setImmediate in I/O');
  });
  
  console.log('I/O 回调结束');
});

// 输出顺序:
// I/O 回调开始
// I/O 回调结束
// setImmediate in I/O
// setTimeout in I/O

六、事件循环性能优化

1. 避免阻塞事件循环

javascript
// 阻塞事件循环的示例
function blockingOperation() {
  const start = Date.now();
  while (Date.now() - start < 1000) {
    // 空循环 1 秒
  }
  console.log('阻塞操作完成');
}

console.log('开始');
blockingOperation();
console.log('结束');

// setTimeout 回调会被延迟执行
setTimeout(() => {
  console.log('setTimeout 回调');
}, 0);

// 输出顺序:
// 开始
// 阻塞操作完成
// 结束
// setTimeout 回调(延迟 1 秒后执行)

2. 使用 Worker Threads 处理 CPU 密集任务

javascript
// main.js
const { Worker } = require('worker_threads');

console.log('主线程开始');

// 启动工作线程处理 CPU 密集任务
const worker = new Worker('./worker.js');

worker.on('message', (result) => {
  console.log('工作线程返回结果:', result);
});

worker.on('error', (error) => {
  console.error('工作线程错误:', error);
});

worker.on('exit', (code) => {
  console.log('工作线程退出,退出码:', code);
});

console.log('主线程继续执行');

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

// worker.js
const { parentPort } = require('worker_threads');

// CPU 密集任务
function cpuIntensiveTask() {
  let result = 0;
  for (let i = 0; i < 1000000000; i++) {
    result += i;
  }
  return result;
}

const result = cpuIntensiveTask();
parentPort.postMessage(result);

3. 合理使用异步操作

javascript
// 同步操作阻塞事件循环
const fs = require('fs');

console.log('开始');

// 同步读取大文件会阻塞事件循环
const data = fs.readFileSync('large-file.txt');
console.log('文件大小:', data.length);

console.log('结束');

// 异步操作不会阻塞事件循环
console.log('开始');

fs.readFile('large-file.txt', (err, data) => {
  if (err) throw err;
  console.log('文件大小:', data.length);
});

console.log('结束');

// 输出顺序:
// 开始
// 结束
// 文件大小: ...

七、事件循环调试技巧

1. 使用 async_hooks 追踪异步资源

javascript
const async_hooks = require('async_hooks');

const asyncHook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    console.log(`初始化异步资源: ${type} (${asyncId})`);
  },
  before(asyncId) {
    console.log(`执行前: ${asyncId}`);
  },
  after(asyncId) {
    console.log(`执行后: ${asyncId}`);
  },
  destroy(asyncId) {
    console.log(`销毁异步资源: ${asyncId}`);
  }
});

asyncHook.enable();

// 测试异步操作
setTimeout(() => {
  console.log('setTimeout 回调');
}, 100);

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

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

2. 使用 console.time() 和 console.timeEnd() 测量执行时间

javascript
// 测量事件循环阶段耗时
console.time('事件循环阶段');

setTimeout(() => {
  console.time('setTimeout');
  // 模拟耗时操作
  const start = Date.now();
  while (Date.now() - start < 100) {
    // 空循环 100ms
  }
  console.timeEnd('setTimeout');
}, 0);

setImmediate(() => {
  console.time('setImmediate');
  // 模拟耗时操作
  const start = Date.now();
  while (Date.now() - start < 50) {
    // 空循环 50ms
  }
  console.timeEnd('setImmediate');
});

console.timeEnd('事件循环阶段');

八、总结

Node.js 事件循环是其高性能并发处理的核心机制。理解事件循环的六个阶段、微任务与宏任务的区别、以及如何避免阻塞事件循环对于编写高效的 Node.js 应用至关重要:

  1. 六个阶段:timers → pending callbacks → idle/prepare → poll → check → close callbacks
  2. 任务类型:微任务(process.nextTick, Promise)优先于宏任务(setTimeout, setImmediate)
  3. 性能优化:避免阻塞操作,合理使用异步操作和 Worker Threads
  4. 调试技巧:使用 async_hooks 和时间测量工具追踪事件循环行为

通过深入理解事件循环机制,我们可以编写出更加高效和稳定的 Node.js 应用程序。