Node 事件循环(Event-Loop)
Node.js 是通过事件驱动来服务 I/O 的,而事件循环是 Node 实现异步非阻塞 I/O 的基础
Node 事件循环是基于 libuv 实现的,而内部实现,其实就是一个 while 语句块
Node 程序启动时,则会初始化事件循环
这里需要说明下,Node 事件循环与浏览器环境下 JavaScript 的事件循环是完全不一样的
Node 事件循环是分阶段的,有 update time、timer、 pending (I/O) callbacks、idle, prepare、poll(轮询)、check、close callbacks 七个阶段
而这七个阶段都是在核心函数 uv_run() 中进行
|
|
各阶段简述
- update time:更新时间
- timer:执行到期的定时器回调函数
- pending (I/O) callbacks:执行延迟到下一个循环迭代的 I/O 回调
- idle, prepare:Node 内部执行的
- poll:检索新的 I/O事件;执行I / O相关的回调函数(几乎所有函数的回调函数,除了定时器和setImmediate);此外,适当时,节点将在此处阻塞
- check:执行 setImmediate 回调
- close callbacks:一些关闭回调,如 socket.on(‘close’, …)
poll 阶段(重点理解)
poll阶段有两个功能:
- 当 timers 到达指定的时间后,执行指定的 timer 的回调
- 处理 poll 队列的事件
1、当进入到 poll 阶段,并且没有 timers 被调用的时候,会发生下面的情况:
- 如果 poll 队列不为空,Event Loop 将同步的执行 poll queue 里的 callback ,直到 queue 为空或者执行的 callback 到达上限
- 如果 poll 队列为空,则会发生下面的情况:
- 如果脚本调用了 setImmediate(),Event Loop 将会结束 poll 阶段并且进入到 check 阶段执行 setImmediate() 的回调
- 如果脚本没有被 setImmediate() 调用,Event Loop 将会等待(此时会阻塞极短时间32.767毫秒)回调被添加到队列中,然后立即执行它们
2、当进入到 poll 阶段,并且调用了 timers 的话,会发生下面的情况:
- 一旦 poll queue 是空的话,Event Loop 会检查是否 timers,如果有1个或多个 timers 时间已经到达,Event Loop 将会回到 timer 阶段并执行那些 timers 的 callback (即进入到下一次 tick)。
Pending callbacks(即I/O callbacks)被调用,大多数情况下,所有的 I/O callbacks 都是在 poll for I/O(即 poll 阶段)后理解调用的。然而,有些情况,会在下一次 tick 调用,以前被推迟的 I/O callback 会在下一次 tick 的 I/O 阶段调用。
在 Event Loop 完成一个阶段,然后到另一个阶段之前,Event Loop 将会执行这 Next Tick Queue 以及 MicroTask Queue 里面的回调,直到这两个队列为空。一旦它们空了后,Event Loop 会进入到下一个阶段
Next Tick Queue的优先级高于MicroTask Queue
setImmediate() vs setTimeout()
setImmediate和setTimeout()是相似的,但取决于它们何时被调用,其行为方式不同。
- setImmediate()用于在当前 poll(轮询)阶段完成后执行脚本
- setTimeout()设置在一定时间(以毫秒为单位)后运行
定时器执行的顺序取决于它们被调用的上下文
如果在 I/O 周期内这两个被调用,则 setImmediate 回调总是先执行
如果它们不在 I/O 周期内被调用,则两者执行顺序是不确定的
process.nextTick() vs setImmediate()
process.nextTick() 属于 idle 观察者,setImmediate() 属于 check 观察者,idle 观察者优先级大于 I/O 观察者,I/O 观察者优先级大于 check 观察者,因此 process.nextTick() 总是先执行