事件循环
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 层:暴露
fs、net、http等模块 - 事件循环:单线程执行 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 阶段
执行已到期的 setTimeout 和 setInterval 回调:
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 还有两个微任务队列:
执行顺序
- 执行同步代码
- 清空
process.nextTick队列 - 清空 Promise 微任务队列
- 进入下一个事件循环阶段
process.nextTick 队列
Node.js 特有,优先级最高:
javascript
process.nextTick(() => {
console.log("nextTick")
})
Promise.resolve().then(() => {
console.log("Promise")
})
console.log("同步")
// 输出:
// 同步
// nextTick
// PromisePromise 微任务队列
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 回调中,
setImmediate比setTimeout(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')