7.Node.js Event Loop - 标准答案

考察点:Node.js 核心原理、事件循环、宏任务微任务

问题:解释 Node.js 的 Event Loop 机制,并说明以下代码的执行顺序:

console.log(1)

setTimeout(() => {
  console.log(2)
  Promise.resolve().then(() => console.log(3))
}, 0)

Promise.resolve().then(() => console.log(4))

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

console.log(6)

1
6
4
2
3
5
执行栈:
1. console.log(1)  → 输出 1
2. setTimeout(..., 0) → 注册宏任务(timers 队列)
3. Promise.resolve().then(...) → 注册微任务
4. setTimeout(..., 0) → 注册宏任务(timers 队列)
5. console.log(6)  → 输出 6

同步代码执行完毕,开始 Event Loop:

第1轮循环:
  - 清空微任务队列:console.log(4) → 输出 4
  - 执行宏任务(timers):第1个 setTimeout
    - console.log(2) → 输出 2
    - Promise.resolve().then(...) → 注册微任务
  - 清空微任务队列:console.log(3) → 输出 3

第2轮循环:
  - 执行宏任务(timers):第2个 setTimeout
    - console.log(5) → 输出 5

   ┌───────────────────────────┐
┌─>│           timers          │  执行 setTimeout/setInterval 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  执行延迟到下一轮的 I/O 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  仅内部使用
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │  执行 setImmediate 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │  执行 socket.on('close', ...) 回调
   └───────────────────────────┘

各阶段说明

阶段说明示例
timers执行 setTimeoutsetInterval 回调setTimeout(fn, 0)
pending callbacks执行系统操作的回调(如 TCP 错误)-
idle, prepare内部使用-
poll获取新的 I/O 事件,执行 I/O 回调fs.readFile
check执行 setImmediate 回调setImmediate(fn)
close callbacks执行 close 事件回调socket.on('close', fn)

微任务在每个阶段结束后执行

// 微任务类型
process.nextTick(callback)  // 优先级最高
Promise.resolve().then(callback)
queueMicrotask(callback)
async/await

执行顺序

每个宏任务阶段结束后:
1. 清空 process.nextTick 队列
2. 清空 Promise 微任务队列
3. 进入下一个宏任务阶段

// 情况1:在主模块中(非 I/O 回调)
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))

// 输出顺序不确定!
// 原因:setTimeout(fn, 0) 实际是 setTimeout(fn, 1)
//       如果事件循环启动用时 < 1ms,先执行 setImmediate
//       如果事件循环启动用时 >= 1ms,先执行 setTimeout
// 情况2:在 I/O 回调中
const fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => console.log('timeout'), 0)
  setImmediate(() => console.log('immediate'))
})

// 输出顺序固定:
// immediate
// timeout

// 原因:I/O 回调在 poll 阶段执行
//       setImmediate 在下一个 check 阶段立即执行
//       setTimeout 需要等到下一轮 Event Loop 的 timers 阶段

console.log('start')

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

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

console.log('end')

// 输出顺序:
// start
// end
// nextTick 1
// nextTick 2  ← nextTick 优先级更高,递归执行完
// promise 1
// promise 2

优先级

process.nextTick > Promise.then > setTimeout > setImmediate

console.log('1')

setTimeout(() => {
  console.log('2')
  process.nextTick(() => console.log('3'))
}, 0)

setTimeout(() => {
  console.log('4')
  process.nextTick(() => console.log('5'))
}, 0)

process.nextTick(() => console.log('6'))

console.log('7')

// 输出顺序分析:

// 同步代码:
1, 7

// 第1轮 Event Loop:
// - 微任务(process.nextTick):
6

// - 宏任务(timers 第1个 setTimeout):
2
// - 微任务(process.nextTick):
3

// - 宏任务(timers 第2个 setTimeout):
4
// - 微任务(process.nextTick):
5

// 最终输出:
// 1, 7, 6, 2, 3, 4, 5

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')  // 相当于 Promise.then
}

async function async2() {
  console.log('async2')
}

console.log('script start')

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

async1()

new Promise(resolve => {
  console.log('promise1')
  resolve()
}).then(() => {
  console.log('promise2')
})

console.log('script end')

// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end  ← await 后面的代码进入微任务队列
// promise2
// setTimeout

await 的本质

// 这段代码:
async function fn() {
  await somePromise()
  console.log('after await')
}

// 等价于:
function fn() {
  return somePromise().then(() => {
    console.log('after await')
  })
}

特性浏览器Node.js
宏任务setTimeout、setInterval、I/O、UI 渲染setTimeout、setImmediate、I/O
微任务Promise、MutationObserverPromise、process.nextTick
执行顺序每次执行 1 个宏任务 → 清空所有微任务每个阶段执行所有宏任务 → 清空所有微任务
特殊 APIprocess.nextTick(优先级最高)

setTimeout(() => console.log('timeout'), 0)

// 实际延迟:
// - 浏览器:至少 4ms(HTML5 标准)
// - Node.js:至少 1ms
// ❌ 危险代码
function recursiveNextTick() {
  process.nextTick(recursiveNextTick)
}
recursiveNextTick()

// 问题:nextTick 优先级太高,会一直递归,阻塞 Event Loop
// 其他 I/O 操作永远无法执行
setTimeout(() => {
  console.log('timeout 1')
  Promise.resolve().then(() => console.log('promise'))
  setTimeout(() => console.log('timeout 2'), 0)
}, 0)

// 输出顺序:
// timeout 1
// promise  ← 微任务插队,比 timeout 2 先执行
// timeout 2

  1. 理解宏任务和微任务的区别
  2. 避免滥用 process.nextTick(可能阻塞 Event Loop)
  3. I/O 回调中优先使用 setImmediate(而非 setTimeout)
  4. 长任务分片:避免阻塞 Event Loop
// ❌ 阻塞 Event Loop
for (let i = 0; i < 1e9; i++) {
  // 耗时操作
}

// ✅ 分片处理
function processData(data, batchSize = 1000) {
  let index = 0
  
  function processBatch() {
    const end = Math.min(index + batchSize, data.length)
    
    for (let i = index; i < end; i++) {
      // 处理数据
    }
    
    index = end
    
    if (index < data.length) {
      setImmediate(processBatch)  // 让出 Event Loop
    }
  }
  
  processBatch()
}
  1. 使用 Worker Threads 处理 CPU 密集型任务
const { Worker } = require('worker_threads')

const worker = new Worker('./heavy-task.js')
worker.on('message', result => {
  console.log('Result:', result)
})