Appearance
Nodejs 事件循环
理解
非阻塞 I/O 操作的设计机制
- Timer: 定时器运行 setTimeout setInterval
- Pending Callback: 挂起的回调,执行延迟到下一个循环迭代的 I/O 回调(TCP 的错误处理延迟到这里执行)
- Idle, Prepare: 仅在内部使用
- Poll: 轮询,检索新的 I/O 事件,执行与 I/O 相关的回调(计时器和 setImmediated()调度之外)
- 计算应该阻塞和轮询 I/O 的时间
- 处理轮询队列中的事件(没有被调度的计时器时)
- 轮询队列为空
- 如果脚本被 setImmediate() 调度,则事件结束轮询阶段,并到 Check 阶段继续执行 setImmediate() 调度的脚本
- 未被 setImmediate() 调度,则事件循环将等待回调被添加到队列中,然后立即执行
- 轮询队列不为空
- 事件循环将循环访问回调队列并执行他们,直到队列用尽,或者达到了与系统相关的硬性限制
- 轮询队列为空
- Check: setImmeidate 检查是否执行
- Close Callback: 关闭的回调函数(socket.on('close))
process.nextTick 又是如何执行?
- 异步 API
任何时候再给定的阶段中调用 process.nextTick(),所以传递到 process.nextTick() 的回调将在事件循环继续之前解析。
- 造成任务饿死
一直调用 process.nextTick() 从而导致事件循环无法到达轮询阶段(递归调用 process.nextTick() 不允许超过 V8 的最大调用堆栈大小)
事件循环图
┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘
定时器 timers
本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数
- 执行所提供的回调的阈值,而不是用户希望其执行的确切时间(计时器回调尽可能早的运行,但是操作系统调度或其它正在运行的回调可能会存在延迟)
js
// 100ms !== 阈值
setTimeout(function() {}, 100);// 100ms !== 阈值
setTimeout(function() {}, 100);- 轮询阶段控制何时定时器执行
待定的回调 pending callbacks
执行延迟到下一个循环迭代的 I/O 回调
- 挂起的回调函数(持续连接 TCP 套接字在尝试连接时接收到
ECONNREFUSED)
idle prepare
系统内部使用
轮询 poll
检索新的 I/O 事件,执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,计时器和 setImmediate() 调度的之外)其余情况 node 将在适当的时候阻塞
计算应该阻塞和轮询 I/O 的时间
处理轮询队列里的事件
- 轮询队列不为空,事件循环将循环访问回调队列并同步执行他们,直到队列为空,或者达到了与系统相关的硬性限制
- 轮询队列为空,如果脚本被 setImmediate() 调度,则事件循环将结束轮询阶段,并继续检查阶段以执行哪些被调度的脚本;如果脚本未被 setImmediate() 调度,则事件循环将等待回调被添加到队列中,然后立即执行
一旦轮询队列为空,事件循环将检查已达到阈值的计时器,如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行计时器回调
检测 check
setImmediate() 回调函数在这里执行
- 轮询阶段完成后立即执行回调,轮询阶段变为空闲状态,脚本使用 setImmediate() 后被排列在队列中,则事件循环可能继续到检查阶段而不是等待
- setImmediate() 事件循环中的单独阶段运行的特殊及时器(libuv API 安排回调在轮询阶段完成后立即执行)
关闭的回调函数 close callbacks
一些关闭的回调函数 socket.o('close', ...)
- 套接字或处理函数关闭(socket.destroy()),close 事件在这个阶段发出,否则(process.nextTick()) 发出
setTimeout() VS setImmediate()
- setImmediate() 一旦在当前轮询阶段完成,就执行脚本
- setTimeout() 轮询阶段计算出来的阈值(ms)过后运行脚本
- 主模块中运行(非确定性)
- 运行在 I/O 循环内调用,setImmediate 总是被优先调用
process.nextTick
不是事件循环的一部分,在当前操作完成后处理 nextTickQueue,
允许通过递归调用来阻止I/O,阻止事件到达轮询阶段保证用户代码的其余部分之后在让事件循环继续进行之前,执行其回调函数
回调置于 process.nextTick() 中,脚本仍具有运行完成的能力,允许在调用回调之前初始化所有的变量、函数
不让事件循环继续的优点,适用于让事件循环继续之前,警告用户发生错误的情况
process.nextTick() VS setImmediate()
- process.nextTick() 在同一个阶段立即执行
- setImmeidate() 在事件循环的接下来的 tick 上触发
为什么使用 process.nextTick()
- 允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求
- 让回调在栈展开后,但在事件循环继续之前运行的必要