Appearance
事件循环
什么是事件循环
事件循环(Event Loop)是 Node.js 处理异步操作的核心机制。它负责处理所有的异步操作、事件和回调函数,使得 Node.js 能够在单线程环境下高效地处理并发请求。
Node.js 的架构
1. V8 引擎
- 执行 JavaScript 代码
- 内存管理
- 垃圾回收
2. Libuv
- 跨平台的异步 I/O 库
- 事件循环的实现
- 线程池管理
- 文件系统操作
- 网络操作
3. 核心模块
- fs(文件系统)
- http(HTTP 服务器)
- net(网络)
- dns(域名解析)
4. 第三方模块
- 来自 npm 的包
事件循环的阶段
Node.js 的事件循环分为以下几个阶段:
1. timers 阶段
- 执行 setTimeout() 和 setInterval() 的回调
- 检查定时器是否到期
2. pending callbacks 阶段
- 执行 I/O 操作的回调
- 处理上一轮循环中未执行的回调
3. idle, prepare 阶段
- 内部使用
- 仅在 libuv 内部使用
4. poll 阶段
- 执行 I/O 操作
- 检查新的 I/O 事件
- 执行 I/O 回调
- 计算阻塞时间
5. check 阶段
- 执行 setImmediate() 的回调
6. close callbacks 阶段
- 执行关闭事件的回调
- 如 socket.on('close', ...)
事件循环的执行流程
- 执行同步代码
- 执行微任务(process.nextTick() 和 Promise 回调)
- 进入事件循环
- 执行 timers 阶段的回调
- 执行 pending callbacks 阶段的回调
- 执行 idle, prepare 阶段
- 执行 poll 阶段:
- 如果有 I/O 事件,执行回调
- 如果没有 I/O 事件,等待新的事件
- 执行 check 阶段的回调
- 执行 close callbacks 阶段的回调
- 重复以上过程
微任务和宏任务
微任务
- process.nextTick()
- Promise 的 then()、catch()、finally() 回调
- Async/Await(基于 Promise)
宏任务
- setTimeout()
- setInterval()
- setImmediate()
- I/O 操作
- 事件回调
process.nextTick()
- 优先级高于 Promise
- 在当前操作完成后立即执行
- 即使在事件循环的不同阶段之间
setImmediate() vs setTimeout()
- setImmediate() 在 check 阶段执行
- setTimeout() 在 timers 阶段执行
- 如果在主模块中同时调用,执行顺序取决于系统性能
- 如果在 I/O 回调中调用,setImmediate() 总是先于 setTimeout() 执行
事件循环的最佳实践
1. 避免阻塞操作
- 不要在事件循环中执行 CPU 密集型操作
- 使用 worker threads 处理 CPU 密集型任务
- 合理使用异步 I/O
2. 合理使用定时器
- 根据需求选择合适的定时器
- 避免过多的定时器
- 注意定时器的精度
3. 处理错误
- 正确处理异步操作的错误
- 使用 try-catch 处理同步错误
- 使用错误事件监听
4. 内存管理
- 避免内存泄漏
- 及时释放不再使用的资源
- 合理使用缓存
常见问题
1. 回调地狱
- 嵌套的回调函数
- 代码难以维护
- 解决方案:使用 Promise 或 Async/Await
2. 事件循环阻塞
- CPU 密集型操作
- 同步 I/O 操作
- 解决方案:使用 worker threads 或子进程
3. 内存泄漏
- 未释放的事件监听器
- 闭包引用
- 解决方案:及时移除事件监听器,避免循环引用
实践练习
- 编写一个示例,演示事件循环的不同阶段
- 比较 setImmediate() 和 setTimeout() 的执行顺序
- 演示微任务和宏任务的执行顺序
- 解决一个回调地狱问题,使用 Promise 或 Async/Await