后端开发
深入理解Node.js事件循环机制
详细解析Node.js事件循环的工作原理,包括宏任务、微任务的执行顺序,以及在实际开发中的应用场景。
湖边工程师
2024-03-10
6 分钟
技术文章
Node.jsJavaScript事件循环异步编程
Node.js的事件循环是其高性能的核心所在。理解事件循环机制对于编写高效的Node.js应用至关重要。
事件循环基础
什么是事件循环?
事件循环是一个执行模型,它使得Node.js能够执行非阻塞I/O操作。尽管JavaScript是单线程的,但通过将操作卸载到系统内核,Node.js可以实现高并发。
console.log('开始');
setTimeout(() => {
console.log('setTimeout 回调');
}, 0);
setImmediate(() => {
console.log('setImmediate 回调');
});
console.log('结束');
// 输出顺序在不同环境下可能不同
事件循环的六个阶段
事件循环的每一轮称为一个"tick",每个tick分为六个阶段:
┌───────────────────────────┐
┌─>│ timers │ <-- 执行setTimeout()和setInterval()的回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ <-- 执行延迟到下一个循环迭代的I/O回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ <-- 仅系统内部使用
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ <-- 获取新的I/O事件;执行与I/O相关的回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ <-- setImmediate()回调函数在这里执行
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ <-- 一些准备关闭的回调函数
└───────────────────────────┘1. Timers 阶段
执行setTimeout()和setInterval()的回调函数。
console.log('start');
setTimeout(() => {
console.log('timer1');
}, 0);
setTimeout(() => {
console.log('timer2');
}, 0);
console.log('end');
// 输出: start -> end -> timer1 -> timer2
2. Pending callbacks 阶段
执行延迟到下一个循环迭代的I/O回调。
3. Idle, prepare 阶段
仅供系统内部使用。
4. Poll 阶段(轮询阶段)
这是最重要的阶段:
- 获取新的I/O事件
- 执行与I/O相关的回调
- 在适当的时候阻塞在此阶段
const fs = require('fs');
console.log('开始');
// I/O操作
fs.readFile('./package.json', () => {
console.log('文件读取完成');
});
console.log('结束');
// 输出: 开始 -> 结束 -> 文件读取完成
5. Check 阶段
执行setImmediate()回调函数。
6. Close callbacks 阶段
执行一些关闭的回调函数,如socket.on('close', ...)。
宏任务与微任务
宏任务 (Macrotask)
setTimeoutsetIntervalsetImmediate- I/O操作
- UI渲染(浏览器环境)
微任务 (Microtask)
Promise.then/catch/finallyprocess.nextTickqueueMicrotask
执行优先级
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
重要规则:
- 同步代码最先执行
- 每个阶段结束后,都会执行微任务队列
process.nextTick优先级最高- Promise 微任务次之
- 宏任务最后执行
深入理解执行顺序
复杂示例
console.log('=== 开始 ===');
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
setTimeout(() => {
console.log('timer2');
Promise.resolve().then(() => {
console.log('promise2');
});
}, 0);
Promise.resolve().then(() => {
console.log('promise3');
process.nextTick(() => {
console.log('nextTick1');
});
});
process.nextTick(() => {
console.log('nextTick2');
Promise.resolve().then(() => {
console.log('promise4');
});
});
console.log('=== 结束 ===');
// 输出分析:
// === 开始 ===
// === 结束 ===
// nextTick2 (process.nextTick优先级最高)
// promise3 (Promise微任务)
// promise4 (nextTick2中的Promise)
// nextTick1 (promise3中的nextTick)
// timer1 (第一个setTimeout)
// promise1 (timer1中的Promise)
// timer2 (第二个setTimeout)
// promise2 (timer2中的Promise)
setImmediate vs setTimeout
// 在I/O回调中
const fs = require('fs');
fs.readFile('./package.json', () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
});
// 在I/O回调中,setImmediate总是优先于setTimeout
// 输出: setImmediate -> setTimeout
// 在主线程中
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
// 在主线程中,执行顺序不确定,取决于系统性能
实际应用场景
1. 避免阻塞主线程
// 不好的做法 - 阻塞主线程
function heavyComputation() {
let result = 0;
for (let i = 0; i < 10000000; i++) {
result += i;
}
return result;
}
console.log('开始');
const result = heavyComputation();
console.log('结果:', result);
console.log('结束');
// 更好的做法 - 使用setImmediate分片处理
function heavyComputationAsync(max, callback) {
let i = 0;
let result = 0;
function chunk() {
let count = 0;
while (count < 100000 && i < max) {
result += i++;
count++;
}
if (i < max) {
setImmediate(chunk); // 让出控制权
} else {
callback(result);
}
}
chunk();
}
console.log('开始');
heavyComputationAsync(10000000, (result) => {
console.log('结果:', result);
console.log('结束');
});
console.log('继续执行其他任务...');
2. 优化I/O操作
const fs = require('fs').promises;
// 串行读取文件(慢)
async function readFilesSequentially(files) {
const results = [];
for (const file of files) {
try {
const content = await fs.readFile(file, 'utf8');
results.push(content);
} catch (error) {
console.error(`读取文件 ${file} 失败:`, error.message);
results.push(null);
}
}
return results;
}
// 并行读取文件(快)
async function readFilesConcurrently(files) {
const promises = files.map(file =>
fs.readFile(file, 'utf8').catch(error => {
console.error(`读取文件 ${file} 失败:`, error.message);
return null;
})
);
return await Promise.all(promises);
}
// 使用示例
const files = ['file1.txt', 'file2.txt', 'file3.txt'];
console.time('串行读取');
readFilesSequentially(files).then(() => {
console.timeEnd('串行读取');
});
console.time('并行读取');
readFilesConcurrently(files).then(() => {
console.timeEnd('并行读取');
});
3. 实现简单的任务调度器
class TaskScheduler {
constructor() {
this.tasks = [];
this.running = false;
}
addTask(task) {
this.tasks.push(task);
if (!this.running) {
this.runTasks();
}
}
runTasks() {
this.running = true;
const runNext = () => {
if (this.tasks.length === 0) {
this.running = false;
return;
}
const task = this.tasks.shift();
try {
const result = task();
// 如果任务返回Promise,等待完成
if (result && typeof result.then === 'function') {
result
.then(() => setImmediate(runNext))
.catch((error) => {
console.error('任务执行失败:', error);
setImmediate(runNext);
});
} else {
setImmediate(runNext);
}
} catch (error) {
console.error('任务执行失败:', error);
setImmediate(runNext);
}
};
setImmediate(runNext);
}
}
// 使用示例
const scheduler = new TaskScheduler();
scheduler.addTask(() => {
console.log('任务1: 同步任务');
});
scheduler.addTask(async () => {
console.log('任务2: 异步任务开始');
await new Promise(resolve => setTimeout(resolve, 100));
console.log('任务2: 异步任务完成');
});
scheduler.addTask(() => {
console.log('任务3: 另一个同步任务');
});
性能监控和调试
1. 监控事件循环延迟
const { performance } = require('perf_hooks');
function measureEventLoopLag() {
const start = performance.now();
setImmediate(() => {
const lag = performance.now() - start;
console.log(`事件循环延迟: ${lag.toFixed(2)}ms`);
});
}
// 定期监控
setInterval(measureEventLoopLag, 1000);
// 模拟CPU密集任务
function cpuIntensiveTask() {
const start = Date.now();
while (Date.now() - start < 100) {
// 占用CPU 100ms
}
}
// 每2秒执行一次CPU密集任务
setInterval(cpuIntensiveTask, 2000);
2. 检测长时间运行的任务
function wrapFunction(fn, name) {
return function(...args) {
const start = process.hrtime.bigint();
const result = fn.apply(this, args);
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1000000; // 转换为毫秒
if (duration > 10) { // 如果执行时间超过10ms
console.warn(`⚠️ 函数 ${name} 执行时间过长: ${duration.toFixed(2)}ms`);
}
return result;
};
}
// 使用示例
const slowFunction = wrapFunction(function() {
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
return sum;
}, 'slowFunction');
slowFunction(); // 会输出警告
最佳实践
1. 合理使用异步操作
// ❌ 错误:阻塞事件循环
function processLargeArray(arr) {
return arr.map(item => heavyOperation(item));
}
// ✅ 正确:分批处理
async function processLargeArrayAsync(arr, batchSize = 100) {
const results = [];
for (let i = 0; i < arr.length; i += batchSize) {
const batch = arr.slice(i, i + batchSize);
const batchResults = batch.map(item => heavyOperation(item));
results.push(...batchResults);
// 让出控制权,允许其他任务执行
await new Promise(resolve => setImmediate(resolve));
}
return results;
}
2. 避免深层回调嵌套
// ❌ 回调地狱
fs.readFile('file1.txt', (err1, data1) => {
if (err1) throw err1;
fs.readFile('file2.txt', (err2, data2) => {
if (err2) throw err2;
fs.writeFile('output.txt', data1 + data2, (err3) => {
if (err3) throw err3;
console.log('完成');
});
});
});
// ✅ 使用async/await
async function processFiles() {
try {
const data1 = await fs.readFile('file1.txt', 'utf8');
const data2 = await fs.readFile('file2.txt', 'utf8');
await fs.writeFile('output.txt', data1 + data2);
console.log('完成');
} catch (error) {
console.error('处理文件失败:', error);
}
}
3. 使用流处理大文件
const fs = require('fs');
const { pipeline } = require('stream');
const { Transform } = require('stream');
// 处理大文件而不阻塞事件循环
function processLargeFile(inputPath, outputPath) {
const transformStream = new Transform({
transform(chunk, encoding, callback) {
// 处理数据块
const processed = chunk.toString().toUpperCase();
callback(null, processed);
}
});
return new Promise((resolve, reject) => {
pipeline(
fs.createReadStream(inputPath),
transformStream,
fs.createWriteStream(outputPath),
(error) => {
if (error) {
reject(error);
} else {
resolve();
}
}
);
});
}
总结
理解Node.js事件循环机制的关键点:
- 单线程特性:JavaScript运行在单线程中,通过事件循环实现并发
- 六个阶段:每个事件循环包含六个不同的阶段
- 任务优先级:微任务优先于宏任务,
process.nextTick优先级最高 - 非阻塞I/O:通过异步操作避免阻塞主线程
- 性能监控:定期监控事件循环延迟,及时发现性能问题
掌握这些概念,你就能够:
- 编写更高效的异步代码
- 避免常见的性能陷阱
- 更好地调试异步问题
- 构建高性能的Node.js应用
记住:异步不等于并发,理解执行顺序是关键!