Appearance
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应用程序至关重要,它帮助开发者避免阻塞操作,合理安排任务执行顺序。