深入理解Node.js事件循环机制
后端开发

深入理解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)

  • setTimeout
  • setInterval
  • setImmediate
  • I/O操作
  • UI渲染(浏览器环境)

微任务 (Microtask)

  • Promise.then/catch/finally
  • process.nextTick
  • queueMicrotask

执行优先级

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

重要规则

  1. 同步代码最先执行
  2. 每个阶段结束后,都会执行微任务队列
  3. process.nextTick 优先级最高
  4. Promise 微任务次之
  5. 宏任务最后执行

深入理解执行顺序

复杂示例

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事件循环机制的关键点:

  1. 单线程特性:JavaScript运行在单线程中,通过事件循环实现并发
  2. 六个阶段:每个事件循环包含六个不同的阶段
  3. 任务优先级:微任务优先于宏任务,process.nextTick优先级最高
  4. 非阻塞I/O:通过异步操作避免阻塞主线程
  5. 性能监控:定期监控事件循环延迟,及时发现性能问题

掌握这些概念,你就能够:

  • 编写更高效的异步代码
  • 避免常见的性能陷阱
  • 更好地调试异步问题
  • 构建高性能的Node.js应用

记住:异步不等于并发,理解执行顺序是关键!

相关文章

湖边工程师

全栈开发工程师 & 独立开发者