浏览器和 Node 中的事件循环区别

浏览器与 Node.js 事件循环机制的差异对比

问题

浏览器和 Node 中的事件循环有什么区别?

解答

浏览器中的事件循环

浏览器的事件循环执行顺序:

  1. 执行一个宏任务(task)
  2. 执行完所有微任务队列(micro-task)
  3. 重复上述过程

宏任务包括: setTimeout、setInterval、script(整体代码)、I/O 操作、UI 渲染等

微任务包括: Promise.then()、MutationObserver 等

console.log('script start');

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

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

console.log('script end');

// 输出顺序:script start -> script end -> promise -> setTimeout

Node 中的事件循环

Node 的事件循环由 libuv 实现,分为 6 个阶段:

  1. timers:执行 setTimeout() 和 setInterval() 的回调
  2. pending callbacks:执行延迟到下一个循环的 I/O 回调
  3. idle, prepare:仅系统内部使用
  4. poll:检索新的 I/O 事件,执行 I/O 相关回调
  5. check:执行 setImmediate() 回调
  6. close callbacks:执行关闭回调,如 socket.on(‘close’, …)

Node 10 及之前:

  • 执行完一个阶段的所有任务
  • 执行 nextTick 队列
  • 执行微任务队列

Node 11 及之后:

  • 与浏览器行为统一
  • 每执行一个宏任务后立即执行微任务队列
setTimeout(() => {
  console.log('timeout1');
  Promise.resolve().then(() => console.log('promise1'));
}, 0);

setTimeout(() => {
  console.log('timeout2');
  Promise.resolve().then(() => console.log('promise2'));
}, 0);

// Node 10: timeout1 -> timeout2 -> promise1 -> promise2
// Node 11+: timeout1 -> promise1 -> timeout2 -> promise2

关键点

  • 浏览器每执行一个宏任务后清空微任务队列,Node 11+ 与此行为一致
  • Node 的事件循环分为 6 个阶段,每个阶段处理特定类型的任务
  • Node 10 及之前会执行完一个阶段的所有宏任务再处理微任务
  • process.nextTick 优先级高于微任务,会在阶段切换前执行
  • setImmediate 在 check 阶段执行,setTimeout 在 timers 阶段执行