事件轮循允许nodejs去执行非阻塞的I/O操作,尽管javascript运行在单线程之上,但在合适时机会尽可能将操作转移给系统内核去执行。
因为大多数现代浏览器内核都是多线程的,它们可以在后台同时处理多个操作,当其中某一操作完成时,内核就会通知nodejs,以让相应的回调callback添加到轮询队列里并最终执行。
当nodejs启动之后,会初始化事件轮询,处理已提供的输入脚本,这些脚本或许会发起异步API调用,调度定时器,或者调用process.nextTick方法,然后开始处理事件轮询。
下面这幅图展示了一个简化的事件轮询的操作顺序概览
注意:上述的每一块都指向事件轮询的某一个阶段
每一个阶段都有一个先进先出的队列去执行回调,然而每一个阶段都是特殊的,通常情况下,当事件轮询进入到一个特定的阶段时,将会执行特定于该阶段的任意操作,然后执行该阶段队列里的回调直到队列被耗尽或是执行了最大回调数。当队列被耗尽或达到了回调上线,事件轮询就将移动到下一个阶段,以此类推
由于任何操作都有可能去调度更多的操作,而且在轮询阶段处理的新事件由内核来进行排队,因为在处理轮询事件时,轮询事件是可以排队的。结果就是一个长时间执行的回调会允许轮询阶段去运行比定时器阀值更长的时间
注意:在windows和Umix/Linux的实现里有一些细微的差别,但对于这个演示并不重要,这里最重要的部分是确实有7到8个步骤,但我们最为关心的是nodejs实际上使用了上面哪些步骤
- timers:这个阶段会执行setTimeout()或setInterval()所安排的回调函数
- pending callbacks:执行延迟到下一个循环周期的I/O回调
- idle,prepare:仅在内部使用
- poll:检测新的I/O事件,执行I/O相关的回调(除了关闭的回调函数,以及定时器及setImmediate()所安排的事件),node会在此处阻塞
- check:setImmediate回调会在这个阶段调用
- close callbacks: 一些关闭回调会在该阶段进行调用,例如:socket.on('close', ...).
在每一次事件循环执行之间,nodejs都会去检查是否正在等待任何异步I/O或定时器,如果确定没有,则关闭干净
定时器会给一个给定的回调分配一个时间阀值,这个阀值并不一定是准确的时间。在指定的阀值时间过去之后,定时器回调会尽可能早的被安排执行。然而操作系统的调度或者其他回调的执行也许会推迟定时器回调的执行
注意:从技术上来说,轮询阶段控制了什么时间定时器会被执行
例如,假设你分配了100ms的超时阀值去执行回调操作,然后你的脚本开启了一个将耗时95ms的异步读取文件的任务
当事件轮询进入到轮询阶段,暂时只有一个空队列(因为此时fs.readFile还没有完成),所以它会等待数秒直到达到最快的定时器阀值。当等待了95ms之后,fs.readFile完成了读取文件,同时它的耗时10ms的回调会被添加到轮询队列里并被执行。当回调完成时,队列里已经没有回调,所以此时事件轮询就会去查看已经到达阀值的最快的定时器,然后回到定时器阶段去执行定时器回调。在这个例子中,你可以看到总的延时从定时器被调度到它的回调执行完成将会耗费105ms。
注意:为了防止事件轮询一直阻塞在轮询阶段,libuv(一个用C语言实现了nodejs事件轮询和各个平台异步行为的库)会指定一个硬性的最大值以防止更多的事件被加入到poll阶段,
这个阶段会执行一些针对于系统操作的回调,例如TCP类型的错误,比如,一个TCP socket在尝试连接的时候收到了ECONNREFUSED错误,一些*nix的系统就想要报告这个错误。这些就会被推入pending callbacks阶段依次执行