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
// 22. 事件循环与多线程的区别
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 12. 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 callback5. 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
// 22. 宏任务(Macrotasks)
包括 setTimeout、setInterval、setImmediate、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 12. 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 23. 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
// setTimeout2. 在 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 应用至关重要:
- 六个阶段:timers → pending callbacks → idle/prepare → poll → check → close callbacks
- 任务类型:微任务(process.nextTick, Promise)优先于宏任务(setTimeout, setImmediate)
- 性能优化:避免阻塞操作,合理使用异步操作和 Worker Threads
- 调试技巧:使用 async_hooks 和时间测量工具追踪事件循环行为
通过深入理解事件循环机制,我们可以编写出更加高效和稳定的 Node.js 应用程序。