{T}

事件循环

Node.js 的异步行为依赖于事件循环(Event Loop)。事件循环由 libuv 提供,它在单线程中调度 I/O、计时器、回调和微任务,使 Node 能够以非阻塞方式处理大量并发操作。

Node.js 事件循环架构

code
┌─────────────────────────────────────────────────────┐
│                    Node.js 应用                      │
└─────────────────────┬───────────────────────────────┘
                      │
┌─────────────────────▼───────────────────────────────┐
│                  V8 JavaScript 引擎                  │
└─────────────────────┬───────────────────────────────┘
                      │
┌─────────────────────▼───────────────────────────────┐
│                     libuv                            │
│  ┌──────────────┐  ┌──────────────┐                │
│  │  事件循环     │  │  线程池       │                │
│  │  (Event Loop)│  │(Thread Pool) │                │
│  └──────────────┘  └──────────────┘                │
│  ┌──────────────┐  ┌──────────────┐                │
│  │  文件系统     │  │  网络 I/O    │                │
│  └──────────────┘  └──────────────┘                │
└─────────────────────────────────────────────────────┘

核心概念

  • V8:执行 JavaScript 代码
  • libuv:提供跨平台的事件循环与线程池,调度 I/O 事件
  • Node API 层:暴露 fsnethttp 等模块
  • 事件循环:单线程执行 JavaScript,背后使用线程池处理耗时 I/O

事件循环的六个阶段

每一轮事件循环会依次经过以下阶段:

code
   ┌───────────────────────┐
┌─>│        timers         │ setTimeout/setInterval
│  └───────────┬───────────┘
│  ┌───────────┴───────────┐
│  │   pending callbacks   │ 执行延迟到下一轮循环的 I/O 回调
│  └───────────┬───────────┘
│  ┌───────────┴───────────┐
│  │       idle, prepare   │ libuv 内部使用
│  └───────────┬───────────┘
│  ┌───────────┴───────────┐
│  │         poll         │ 轮询新的 I/O 事件
│  └───────────┬───────────┘
│  ┌───────────┴───────────┐
│  │        check         │ setImmediate
│  └───────────┬───────────┘
│  ┌───────────┴───────────┐
└──┤    close callbacks    │ socket.on('close')
   └───────────────────────┘

阶段详解

1. timers 阶段

执行已到期的 setTimeoutsetInterval 回调:

javascript
setTimeout(() => {
  console.log("定时器到期")
}, 100)

注意: 定时器指定的延迟不是精确时间,而是最小延迟。

2. pending callbacks 阶段

执行上一轮中延迟的系统回调,如 TCP 错误:

javascript
const socket = net.connect({ port: 9999 })
socket.on("error", (err) => {
  // 这个回调可能在此阶段执行
})

3. idle, prepare 阶段

libuv 内部使用,通常不需要关注。

4. poll 阶段

处理新的 I/O 事件:

javascript
const fs = require("fs")

fs.readFile("data.txt", (err, data) => {
  // 这个回调在 poll 阶段执行
  console.log("文件读取完成")
})

poll 阶段的行为:

  • 如果有到期的定时器,事件循环会回到 timers 阶段
  • 如果有 setImmediate,会进入 check 阶段
  • 如果没有待处理的 I/O,事件循环可能会阻塞

5. check 阶段

执行 setImmediate 回调:

javascript
setImmediate(() => {
  console.log("立即执行")
})

6. close callbacks 阶段

执行关闭句柄的回调:

javascript
socket.on("close", () => {
  console.log("连接关闭")
})

微任务队列

除了上述阶段,Node.js 还有两个微任务队列:

执行顺序

  1. 执行同步代码
  2. 清空 process.nextTick 队列
  3. 清空 Promise 微任务队列
  4. 进入下一个事件循环阶段

process.nextTick 队列

Node.js 特有,优先级最高:

javascript
process.nextTick(() => {
  console.log("nextTick")
})

Promise.resolve().then(() => {
  console.log("Promise")
})

console.log("同步")

// 输出:
// 同步
// nextTick
// Promise

Promise 微任务队列

javascript
Promise.resolve().then(() => {
  console.log("Promise 微任务")
})

async function asyncDemo() {
  await Promise.resolve()
  console.log("await 后的代码")
}

setImmediate vs setTimeout

在主模块中

顺序不确定:

javascript
// 顺序不确定
setTimeout(() => console.log("timeout"), 0)
setImmediate(() => console.log("immediate"))

在 I/O 回调中

setImmediate 一定先执行:

javascript
const fs = require("fs")

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

// 输出:
// immediate
// timeout

原因: I/O 回调在 poll 阶段执行,紧接着进入 check 阶段(setImmediate),然后下一轮才到 timers 阶段(setTimeout)。

process.nextTick 的使用

使用场景

1. 确保异步执行

javascript
function readFileCached(filename, callback) {
  if (cache.has(filename)) {
    // 使用 nextTick 确保回调始终异步执行
    process.nextTick(() => callback(null, cache.get(filename)))
  } else {
    fs.readFile(filename, callback)
  }
}

2. 在 Promise 构造函数中发出错误

javascript
function promisifyCallback(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      try {
        fn.call(this, ...args, (err, result) => {
          if (err) reject(err)
          else resolve(result)
        })
      } catch (err) {
        // 使用 nextTick 确保错误正确传播
        process.nextTick(() => reject(err))
      }
    })
  }
}

避免陷阱

javascript
// ❌ 递归调用会阻塞事件循环
function bad() {
  process.nextTick(bad)
}

// ✅ 使用 setImmediate 让 I/O 有机会执行
function good() {
  setImmediate(good)
}

常见执行顺序示例

示例 1

javascript
console.log("1. 同步开始")

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

setImmediate(() => {
  console.log("3. setImmediate")
})

process.nextTick(() => {
  console.log("4. nextTick")
})

Promise.resolve().then(() => {
  console.log("5. Promise")
})

console.log("6. 同步结束")

// 输出顺序:
// 1. 同步开始
// 6. 同步结束
// 4. nextTick
// 5. Promise
// 2 或 3(不确定)

示例 2:I/O 回调中

javascript
const fs = require("fs")

fs.readFile(__filename, () => {
  console.log("1. readFile")

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

  setImmediate(() => {
    console.log("3. setImmediate")
  })

  process.nextTick(() => {
    console.log("4. nextTick")
  })
})

console.log("0. 同步")

// 输出顺序:
// 0. 同步
// 1. readFile
// 4. nextTick
// 3. setImmediate
// 2. setTimeout

长任务与线程池

问题:阻塞事件循环

javascript
function heavyCompute() {
  let sum = 0
  for (let i = 0; i < 1e9; i++) {
    sum += i
  }
  return sum
}

console.time("compute")
heavyCompute() // 阻塞事件循环
console.timeEnd("compute")

解决方案

1. 分块处理

javascript
async function heavyComputeChunked(total, chunkSize = 1e6) {
  let sum = 0
  
  for (let i = 0; i < total; i += chunkSize) {
    const end = Math.min(i + chunkSize, total)
    for (let j = i; j < end; j++) {
      sum += j
    }
    // 让出控制权
    await new Promise(resolve => setImmediate(resolve))
  }
  
  return sum
}

2. 使用 Worker Threads

javascript
const { Worker } = require("worker_threads")

function runInWorker(data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker("./compute.js", { workerData: data })
    worker.on("message", resolve)
    worker.on("error", reject)
  })
}

const result = await runInWorker({ n: 1e9 })

线程池

libuv 默认提供 4 个线程用于处理阻塞操作:

javascript
// 查看线程池大小
console.log(process.env.UV_THREADPOOL_SIZE) // 默认 4

// 设置线程池大小(启动前设置)
process.env.UV_THREADPOOL_SIZE = 8

使用线程池的操作:

  • 文件系统操作(部分)
  • DNS 查询
  • zlib 压缩/解压
  • crypto 加密操作

监控事件循环

使用 perf_hooks

javascript
const { monitorEventLoopDelay } = require("perf_hooks")

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

setInterval(() => {
  console.log("事件循环延迟:", histogram.mean / 1e6, "ms")
}, 1000)

使用 async_hooks

javascript
const async_hooks = require("async_hooks")

const hook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    console.log("init", { asyncId, type, triggerAsyncId })
  },
  destroy(asyncId) {
    console.log("destroy", asyncId)
  }
})

hook.enable()

最佳实践

1. 避免阻塞事件循环

javascript
// ❌ CPU 密集型任务阻塞事件循环
function bad() {
  for (let i = 0; i < 1e9; i++) {
    // 长时间计算
  }
}

// ✅ 分块处理或使用 Worker Threads
async function good() {
  // 分块处理
  for (let i = 0; i < 1e9; i += 1e6) {
    // 处理一块
    await new Promise(resolve => setImmediate(resolve))
  }
}

2. 合理使用 process.nextTick

javascript
// ❌ 过度使用会导致 I/O 饥饿
process.nextTick(function recursive() {
  process.nextTick(recursive)
})

// ✅ 使用 setImmediate
setImmediate(function recursive() {
  setImmediate(recursive)
})

3. 选择合适的定时器

javascript
// 需要 I/O 后立即执行 → setImmediate
fs.readFile("data.txt", () => {
  setImmediate(() => {
    // 在下一个 check 阶段执行
  })
})

// 需要最小延迟 → setTimeout
setTimeout(() => {
  // 在 timers 阶段执行
}, 0)

小结

  • Node.js 事件循环由 libuv 提供,分为 6 个阶段
  • 微任务(process.nextTick 和 Promise)在每个阶段之间执行
  • process.nextTick 优先级最高,其次是 Promise 微任务
  • 在 I/O 回调中,setImmediatesetTimeout(fn, 0) 更可靠
  • 避免阻塞事件循环,使用分块处理或 Worker Threads
  • 合理使用 process.nextTick,避免 I/O 饥饿
  • 使用 perf_hooks 监控事件循环性能

Node.js 22+ 事件循环新特性

scheduler API

Node.js 22+ 新增 scheduler 全局对象,提供更精确的任务调度:

javascript
// scheduler.yield() - 让出主线程,比 setTimeout(0) 更高效
async function processItems(items) {
  for (const item of items) {
    await scheduler.yield()  // 让出主线程,避免阻塞
    processItem(item)
  }
}

// scheduler.wait() - 等待指定时间
await scheduler.wait(1000)  // 等待 1 秒

// scheduler.postTask() - 按优先级调度任务
scheduler.postTask(() => console.log('低优先级'), { priority: 'background' })
scheduler.postTask(() => console.log('高优先级'), { priority: 'user-blocking' })

perf_hooks 增强

Node.js 22+ 性能监控 API 更加完善:

javascript
import { performance, eventLoopUtilization } from 'node:perf_hooks'

// 事件循环利用率
const elu = eventLoopUtilization()
console.log(`事件循环利用率: ${(elu.utilization * 100).toFixed(2)}%`)

// 性能观察者
const obs = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`${entry.name}: ${entry.duration}ms`)
  }
})
obs.observe({ entryTypes: ['measure', 'function'] })

// 测量异步函数执行时间
performance.mark('start')
await someAsyncOperation()
performance.mark('end')
performance.measure('operation', 'start', 'end')