Skip to content

Node.js 事件循环面试题

1. 什么是事件循环?

问题:什么是事件循环(Event Loop)?它在 Node.js 中起什么作用?

答案: 事件循环是 Node.js 处理异步操作的核心机制,它允许 Node.js 在单线程模型下执行非阻塞 I/O 操作。

主要作用

  • 协调异步操作的执行顺序
  • 管理回调函数的调用时机
  • 实现单线程下的高并发处理
  • 将异步操作的结果传递给相应的回调函数

工作原理

  1. 执行同步代码
  2. 检查是否有异步操作完成
  3. 将完成的异步操作回调放入执行队列
  4. 按顺序执行回调函数
  5. 重复上述过程

2. 事件循环的六个阶段

问题:Node.js 事件循环包含哪些阶段?每个阶段的作用是什么?

答案

事件循环按顺序执行以下六个阶段:

┌───────────────────────────┐
│           timers          │
│     (setTimeout/setInterval) │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│     pending callbacks     │
│   (系统操作的回调,如TCP错误)  │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│     idle, prepare         │
│      (内部使用)            │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           poll            │
│    (获取新的I/O事件)        │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           check           │
│       (setImmediate)      │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│      close callbacks      │
│   (socket.on('close', ...)) │
└───────────────────────────┘

各阶段说明

  1. timers:执行 setTimeout 和 setInterval 的回调
  2. pending callbacks:执行系统操作的回调(如 TCP 错误)
  3. idle, prepare:内部使用,仅内部使用
  4. poll:获取新的 I/O 事件,执行 I/O 回调
  5. check:执行 setImmediate 的回调
  6. close callbacks:执行 close 事件的回调

3. 宏任务和微任务

问题:Node.js 中的宏任务和微任务有哪些?它们的执行顺序是什么?

答案

宏任务(Macrotasks)

  • setTimeout
  • setInterval
  • setImmediate
  • I/O 操作
  • UI 渲染

微任务(Microtasks)

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

执行顺序

  1. 执行当前阶段的同步代码
  2. 执行所有微任务(process.nextTick 优先于 Promise)
  3. 进入下一个事件循环阶段

示例

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

4. setTimeout vs setImmediate

问题:setTimeout 和 setImmediate 有什么区别?

答案

主要区别

特性setTimeoutsetImmediate
阶段timers 阶段check 阶段
最小延迟4ms(实际可能更长)0ms
精度不精确相对精确
使用场景延迟执行I/O 事件后立即执行

执行顺序

javascript
// 在主模块中,顺序不确定
setTimeout(() => {
  console.log('setTimeout');
}, 0);

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

// 在 I/O 回调中,setImmediate 总是先执行
const fs = require('fs');
fs.readFile('file.txt', () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
  });
  // 输出:setImmediate, setTimeout
});

5. process.nextTick

问题:process.nextTick 是什么?它有什么特点?

答案

概念: process.nextTick 是一个特殊的微任务,它在当前操作完成后、事件循环继续之前立即执行。

特点

  • 优先级最高,比 Promise 还快
  • 在当前阶段立即执行,不进入事件循环
  • 可以创建无限循环(危险)

使用场景

javascript
// 1. 确保在异步操作前执行
function asyncOperation(callback) {
  // 同步初始化
  const data = prepareData();
  
  // 确保回调在同步代码后执行
  process.nextTick(() => {
    callback(data);
  });
}

// 2. 错误处理
function riskyOperation() {
  try {
    // 可能抛出错误的操作
    riskyCall();
  } catch (error) {
    // 使用 nextTick 确保错误处理不会中断当前流程
    process.nextTick(() => {
      throw error;
    });
  }
}

// 3. 保持函数异步一致性
function maybeAsync(arg, callback) {
  if (arg) {
    // 同步结果
    const result = computeSync(arg);
    // 保持异步行为一致性
    process.nextTick(() => callback(null, result));
  } else {
    // 异步操作
    computeAsync(callback);
  }
}

警告

javascript
// 危险:创建无限循环
function dangerous() {
  process.nextTick(dangerous);
}
// 这会阻塞事件循环,导致程序无响应

6. Promise 和 process.nextTick 的执行顺序

问题:以下代码的输出顺序是什么?

javascript
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');
});

答案: 输出顺序:

nextTick 1
nextTick 2
Promise 1
Promise 2

原因

  1. process.nextTick 的优先级高于 Promise
  2. 所有 nextTick 回调在当前阶段完成后立即执行
  3. 然后才执行 Promise 的回调

7. 事件循环的完整示例

问题:分析以下代码的输出顺序

javascript
const fs = require('fs');

console.log('Start');

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

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

fs.readFile(__filename, () => {
  console.log('File read');
  
  setTimeout(() => {
    console.log('Timeout 2');
  }, 0);
  
  setImmediate(() => {
    console.log('Immediate 2');
  });
  
  process.nextTick(() => {
    console.log('NextTick in I/O');
  });
  
  Promise.resolve().then(() => {
    console.log('Promise in I/O');
  });
});

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

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

console.log('End');

答案

Start
End
NextTick 1
Promise 1
Timeout 1
Immediate 1
File read
NextTick in I/O
Promise in I/O
Immediate 2
Timeout 2

执行流程

  1. 同步代码:Start, End
  2. 微任务:NextTick 1, Promise 1
  3. Timers 阶段:Timeout 1
  4. Check 阶段:Immediate 1
  5. Poll 阶段:File read(I/O 完成)
  6. I/O 回调中的微任务:NextTick in I/O, Promise in I/O
  7. Check 阶段:Immediate 2
  8. Timers 阶段:Timeout 2

8. 事件循环与 CPU 密集型任务

问题:事件循环如何处理 CPU 密集型任务?有什么问题?

答案

问题: CPU 密集型任务会阻塞事件循环,导致:

  • 异步操作无法及时处理
  • 响应延迟增加
  • 程序性能下降

示例

javascript
// 阻塞事件循环的 CPU 密集型任务
function cpuIntensiveTask() {
  for (let i = 0; i < 1e9; i++) {
    // 大量计算
  }
}

console.log('Start');
setTimeout(() => {
  console.log('Timeout'); // 会被延迟执行
}, 100);

cpuIntensiveTask();
console.log('End');

解决方案

  1. 使用 setImmediate 分段执行
javascript
function processLargeData(data, chunkSize = 1000) {
  let index = 0;
  
  function processChunk() {
    const chunk = data.slice(index, index + chunkSize);
    
    // 处理数据块
    chunk.forEach(item => process(item));
    
    index += chunkSize;
    
    if (index < data.length) {
      // 让出事件循环
      setImmediate(processChunk);
    }
  }
  
  processChunk();
}
  1. 使用 Worker Threads
javascript
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  // 主线程
  const worker = new Worker(__filename, {
    workerData: { start: 1, end: 1e9 }
  });
  
  worker.on('message', result => {
    console.log('结果:', result);
  });
} else {
  // Worker 线程
  const { start, end } = workerData;
  let sum = 0;
  for (let i = start; i <= end; i++) {
    sum += i;
  }
  parentPort.postMessage(sum);
}
  1. 使用子进程
javascript
const { fork } = require('child_process');

const child = fork('./cpu-task.js');
child.send({ task: 'compute', data: largeData });
child.on('message', result => {
  console.log('计算结果:', result);
});

9. 事件循环监控

问题:如何监控和调试事件循环的性能?

答案

监控方法

  1. 使用 process.nextTick 测量延迟
javascript
function measureEventLoopLag() {
  const start = process.hrtime.bigint();
  
  process.nextTick(() => {
    const lag = Number(process.hrtime.bigint() - start) / 1e6; // 毫秒
    console.log(`事件循环延迟: ${lag.toFixed(3)} ms`);
  });
}

setInterval(measureEventLoopLag, 1000);
  1. 使用 async_hooks 监控异步操作
javascript
const async_hooks = require('async_hooks');

const activeOperations = new Map();

const hook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    activeOperations.set(asyncId, { type, startTime: Date.now() });
  },
  destroy(asyncId) {
    const op = activeOperations.get(asyncId);
    if (op) {
      const duration = Date.now() - op.startTime;
      if (duration > 1000) {
        console.warn(`长时间异步操作: ${op.type}, 耗时: ${duration}ms`);
      }
      activeOperations.delete(asyncId);
    }
  }
});

hook.enable();
  1. 使用 clinic.js 进行性能分析
bash
npm install -g clinic
clinic doctor -- node app.js
  1. 使用 Node.js 内置诊断工具
javascript
const { monitorEventLoopDelay } = require('perf_hooks');

const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();

setInterval(() => {
  console.log(`
    事件循环延迟统计:
    最小: ${histogram.min} ns
    最大: ${histogram.max} ns
    平均: ${histogram.mean} ns
    百分位 99: ${histogram.percentile(99)} ns
  `);
  histogram.reset();
}, 5000);

10. 事件循环的最佳实践

问题:使用事件循环有哪些最佳实践?

答案

  1. 避免阻塞事件循环
javascript
// 不好
function processSync(data) {
  return data.map(item => heavyCompute(item));
}

// 好
async function processAsync(data) {
  const results = [];
  for (const item of data) {
    results.push(await heavyComputeAsync(item));
    // 让出事件循环
    await new Promise(resolve => setImmediate(resolve));
  }
  return results;
}
  1. 合理使用 process.nextTick
javascript
// 保持异步接口的一致性
function apiCall(callback) {
  if (cache.has(key)) {
    // 使用 nextTick 保持异步行为
    process.nextTick(() => callback(null, cache.get(key)));
  } else {
    fetchFromDB(key, callback);
  }
}
  1. 使用 setImmediate 进行任务分割
javascript
function processInChunks(items, processFn, chunkSize = 100) {
  let index = 0;
  
  function processChunk() {
    const chunk = items.slice(index, index + chunkSize);
    chunk.forEach(processFn);
    index += chunkSize;
    
    if (index < items.length) {
      setImmediate(processChunk);
    }
  }
  
  processChunk();
}
  1. 优先使用 Promise 和 async/await
javascript
// 推荐
async function fetchData() {
  try {
    const data = await fetchFromAPI();
    return await processData(data);
  } catch (error) {
    console.error('获取数据失败:', error);
    throw error;
  }
}
  1. 监控事件循环健康状态
javascript
const lagMonitor = setInterval(() => {
  const start = Date.now();
  setImmediate(() => {
    const lag = Date.now() - start;
    if (lag > 100) {
      console.warn(`事件循环延迟过高: ${lag}ms`);
    }
  });
}, 5000);
  1. 使用 Worker Threads 处理 CPU 密集型任务
javascript
const { Worker } = require('worker_threads');

function runInWorker(filename, data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(filename, { workerData: data });
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', code => {
      if (code !== 0) reject(new Error(`Worker 退出码: ${code}`));
    });
  });
}