简单-Node.js Event Loop 的理解 Timers,process.nextTick()

当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下顺序六个循环阶段,

  • timer ( 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;)
  • I/O callbacks(执行除了 close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks;)
  • idle,prepare 仅node内部使用;
  • poll() 获取新的I/O事件, 适当的条件下node将阻塞在这里;
    • incomming
    • connections
    • data ,etc
  • check执行setImmediate() 设定的callbacks;
  • close callbacks 比如socket.on(‘close’, callback)的callback会在这个阶段执行.
    每一个阶段都有一个装有callbackDe FIFO queue(队列),当event loop运行到一个指定的阶段时,node将执行该阶段的队列,当队列callback执行完或者执行callbacks数量超过该阶段的上限时,event loop会转入下一下阶段.
    注意上面六个阶段都不包括 process.nextTick()
    poll阶段
    poll阶段是衔接整个event loop各个阶段比较重要的阶段,为了便于后续例子的理解,本文和原文的介绍顺序不一样,本文先讲这个阶段;
    在node.js里,任何异步方法(除timer,close,setImmediate之外)完成时,都会将其callback加到poll queue里,并立即执行。

poll 阶段有两个主要的功能
1 处理poll队列(poll quenue)的事件(callback);
2 执行timers的callback,当到达timers指定的时间时;

如果event loop进入了 poll阶段,且代码未设定timer,将会发生下面情况:

  • 如果poll queue不为空,event loop将同步的执行queue里的callback,直至queue为空,或执行的callback到达系统上限;

  • 如果poll queue为空,将会发生下面情况:

    • 如果代码已经被setImmediate()设定了callback, event loop将结束poll阶段进入check阶段,并执行check阶段的queue (check阶段的queue是 setImmediate设定的)
    • 如果代码没有设定setImmediate(callback),event loop将阻塞在该阶段等待callbacks加入poll queue;

如果event loop进入了 poll阶段,且代码设定了timer:

  • 如果poll queue进入空状态时(即poll 阶段为空闲状态),event loop将检查timers,如果有1个或多个timers时间时间已经到达,event loop将按循环顺序进入 timers 阶段,并执行timer queue.

以上便是整个event loop时间循环的各个阶段运行机制,有了这层理解,我们来看几个例子
注意,例子中给出的时间在不同机器下和同一机器下不同执行时刻,其值都会有差异
example 1

简单-Node.js Event Loop 的理解 Timers,process.nextTick()_第1张图片

解释:
当时程序启动时,event loop初始化:

1 timer阶段(无callback到达,setTimeout需要10毫秒)
2 i/o callback阶段,无异步i/o完成
3 忽略
4 poll阶段,阻塞在这里,当运行2ms时,fs.readFile完成,将其callback加入 poll队列,并执行callback, 其中callback要消耗20毫秒,等callback之行完,poll处于空闲状态,由于之前设定了timer,因此检查timers,发现timer设定时间是20ms,当前时间运行超过了该值,因此,立即循环回到timer阶段执行其callback,因此,虽然setTimeout的20毫秒,但实际是22毫秒后执行。

example 2

简单-Node.js Event Loop 的理解 Timers,process.nextTick()_第2张图片

解释:
当时程序启动时,event loop初始化:

1 timer阶段(无callback到达,setTimeout需要10毫秒)
2 i/o callback阶段,无异步i/o完成
3 忽略
4 poll阶段,阻塞在这里,当运行5ms时,poll依然空闲,但已设定timer,且时间已到达,因此,event loop需要循环到timer阶段,执行setTimeout callback,由于从poll --> timer中间要经历check,close阶段,这些阶段也会消耗一定时间,因此执行setTimeout callback实际是7毫秒 然后又回到poll阶段等待异步i/o完成,在9毫秒时fs.readFile完成,其callback加入poll queue并执行。

setTimeout 和 setImmediate
二者非常相似,但是二者区别取决于他们什么时候被调用.

  • setImmediate 设计在poll阶段完成时执行,即check阶段;
  • setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行;但其在timer阶段执行

其二者的调用顺序取决于当前event loop的上下文,如果他们在异步i/o callback之外调用,其执行先后顺序是不确定的

setTimeout(function timeout () { console.log('timeout');},0);
setImmediate(function immediate () { console.log('immediate');});

$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

关于这点的原因
在node中,setTimeout(cb, 0) === setTimeout(cb, 1);
而setImmediately属于uv_run_check的部分
确实每次loop进来,都是先检查uv_run_timer的,但是由于cpu工作耗费时间,比如第一次获取的hrtime为0
那么setTimeout(cb, 1),超时时间就是loop->time = 1(ms,node定时器精确到1ms,但是hrtime是精确到纳秒级别的)
所以第一次loop进来的时候就有两种情况:
1.由于第一次loop前的准备耗时超过1ms,当前的loop->time >=1 ,则uv_run_timer生效,timeout先执行
2.由于第一次loop前的准备耗时小于1ms,当前的loop->time = 0,则本次loop中的第一次uv_run_timer不生效,那么io_poll后先执行uv_run_check,即immediate先执行,然后等close cb执行完后,继续执行uv_run_timer

那么你说的为什么在回调中,一定是先immediate执行呢,其实也很容易理解你可以思考一下你写的场景
由于你的timeout和immediate的事件注册是在readFile的回调执行时,触发的所以必然的,在readFile的回调执行前的每一次event loop进来的uv_run_timer都不会有超时事件触发
那么当readFile执行完毕,kevent收到监听的fd事件完成后,执行了该回调,此时
1.timeout事件注册
2.immediate事件注册
3.由于readFile的回调执行完毕,那么就会从uv_io_poll中出来,此时立即执行uv_run_check,所以immediate事件被执行掉
4.最后的uv_run_timer检查timeout事件,执行timeout事件

所以你会发现,在I/O回调中注册的两者,永远都是immediately先执行

但当二者在异步i/o callback内部调用时,总是先执行setImmediate,再执行setTimeout

var fs = require('fs')fs.readFile(__filename, () => 
{ setTimeout(() => {
 console.log('timeout')
 }, 0) 
setImmediate(() => { 
console.log('immediate') 
})})
$ node timeout_vs_immediate.js
immediate
timeout

理解了event loop的各阶段顺序这个例子很好理解:因为fs.readFile callback执行完后,程序设定了timer 和 setImmediate,因此poll阶段不会被阻塞进而进入check阶段先执行setImmediate,后进入timer阶段执行setTimeout

process.nextTick()###

process.nextTick()不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。
![]6@4Y@3JMH(~95A`1.png](http://upload-images.jianshu.io/upload_images/1058258-4edc9f71a2b32cda.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
从poll —> check阶段,先执行process.nextTick,
nextTick1
nextTick2
然后进入check,setImmediate,
setImmediate
执行完setImmediate后,出check,进入close callback前,执行process.nextTick
nextTick3
最后进入timer执行setTimeout
setTimeout

process.nextTick()是node早期版本无setImmediate时的产物,node作者推荐我们尽量使用setImmediate。

简单-Node.js Event Loop 的理解 Timers,process.nextTick()_第3张图片
简单-Node.js Event Loop 的理解 Timers,process.nextTick()_第4张图片

功能上
这两个都能接受一个传入的function()作为参数,延迟执行。
但是在行为上
process.nextTick()在每轮事件循环中会将数组中的回调函数全部执行,而setImmediate()只会执行链表中的一个回调函数。
但是这就带来了一个问题,使用process.nextTick()推入的回调函数将会顺序执行,在数组中的回调函数执行完之前,都不会进入下次事件循环,如果数组中有一个回调函数执行时间很长,那么其他正在等待执行的回调函数就会处于长时间等待的状态。
因此,当我们需要避免这种I/O回调函数因为process.nextTick()而处于长时间等待的情况时,我们应该使用setImmediate()执行需要延迟执行的任务,因为它在每轮事件循环中只会执行一个回调函数。

你可能感兴趣的:(简单-Node.js Event Loop 的理解 Timers,process.nextTick())