什么是事件循环?众所周知,JavaScript是单线程的,Nodejs因为EventLoop的存在可以实现非阻塞I/O操作。EventLoop主要有以下几个阶段。一个长方形代表一个舞台,如下图:┌──────────────────────────────────────┐┌─>│定时器...──┐││I/O回调││└────────────┬───────────────────────────────────────────────────────────────────────────────────────────┘┌──────────────────────────────────────────────────────────┴──────────────┐││空闲,准备││└────────────┬────────────────────┘┌────────────────────┐│┌──────────┴────────────────┐┐传入:│││投票│<────────┤连接,││└────────────┬────────────────┘│数据等││┌───────────────────────────────────────────┴──────────────┐──────────────────┘│检查│└──────────────────────────────┘│┌────────────┴──────────────┐└──┤平仓回调│└──────────────────────────────┘每个阶段都会维护一个先进先出的队列结构。当事件循环进入到某个阶段时,会执行该阶段的一些特定操作。然后依次执行队列中的回调函数。当队列中的回调函数全部执行完或者执行的回调函数数量达到某个最大值时,进入下一阶段。在定时器事件循环的开始,setTimeout和setInterval的回调函数被执行。当定时器指定的时间到了,定时器的回调函数会被放入队列中,然后依次执行。假设你有四个定时器a、b、c,时间间隔分别为10ms、20ms、30ms。当进入事件循环的timer阶段,经过了25ms,会执行timera和b的回调,执行完后进入下一阶段。I/O回调执行除setTimeout、setInterval、setImmediate和close回调之外的回调函数。空闲,准备一些内部操作。poll这应该是事件循环中最重要的阶段。如果这个阶段的队列不为空,则队列中的回调会依次执行;如果队列为空,调用了setImmediate函数,则进入检查阶段。如果队列为空且没有setImmediate函数调用,事件循环会等待,一旦有回调函数加入队列,就会立即执行。checksetImmediate的回调将在这个阶段执行。关闭回调如socket.on('close',...)在此阶段执行。setTimoutvssetImmediate//timeout_vs_immediate_1.jssetTimeout(functiontimeout(){console.log('timeout');},0);setImmediate(functionimmediate(){console.log('immediate');});按照前面的说法,事件循环会先进入timer阶段执行setTimeout的回调,进入check阶段才会执行setImmediate的回调。所以有人认为上面代码的输出应该是:$nodetimeout_vs_immediate_1.jstimeoutimmediate但实际上这里的结果是不确定的。这往往与进程的性能有关,而且,虽然这里的setTimeout的间隔是0,但实际上会是1。所以当启动程序进入事件循环,1ms还没有过去的时候,timer阶段的队列是为空,不会执行任何回调。而这里调用了setImmediate函数,所以到了check阶段,就会调用setImmediate的回调。如果事件循环在进入timer阶段时已经消耗了1ms,那么此时会执行setTimeout回调,在进入check阶段之前,再执行setImmediate回调。因此,以下两种输出都是可能的。$nodetimeout_vs_immediate_1.jstimeoutimmediate$nodetimeout_vs_immediate_1.jsimmediatetimeout如果,上面的代码放在一个I/0循环中,比如//timeout_vs_immediate_2.jsconstfs=require('fs');fs.readFile(__filename,()=>{setTimeout(()=>{console.log('timeout');},0);setImmediate(()=>{console.log('immediate');});});那么结果是确定的,输出如下$nodetimeout_vs_immediate_2.jsimmediatetimeoutprocess.nextTick()process.nextTick()不是事件循环的一部分,但也是一个异步API。当前操作完成后,如果调用了process.nextTick(),接下来会执行process.nextTick()中的回调。如果回调中还有另一个process.nextTick(),那么接下来会执行process.nextTick()回调。因此procee.nextTick()可能会阻止事件循环进入下一阶段。//process_nexttick_1.jsleti=0;functionfoo(){i+=1;如果(i>3)返回;console.log('foofunc');setTimeout(()=>{console.log('timeout');},0);process.nextTick(foo);}setTimeout(foo,5);根据前面的说法,上面的输出是这样的:$nodeprocess_nexttick_1.jsfoofuncfoofuncfoofunctimeouttimeouttimeout你可能会有疑问,process.nextTick()是在事件循环的某个阶段的队列清空后执行的,还是在执行队列中的回调之后。想想下面代码的输出?//process_nexttick_2.jsleti=0;functionfoo(){i+=1;如果(i>2)返回;console.log('foofunc');setTimeout(()=>{console.log('timeout');},0);process.nextTick(foo);}setTimeout(foo,2);setTimeout(()=>{console.log('另一个超时');},2);执行看看Result//nodeversion:v11.12.0$nodeprocess_nexttick_2.jsfoofuncfoofuncanothertimeouttimeouttimeoutResult如上所示,process.nextTick()是在队列中的回调完成后执行的。你看到上面的结果,有一个注释节点版本:v11.12.0。即运行这段代码的node版本为11.12.0。如果你的node版本低于这个,比如7.10.1,可能会得到不同的结果。//nodeversion:v7.10.0$nodeprocess_nexttick_2.jsfoofuncanothertimeoutfoofunctimeouttimeout不同版本表现不一样,我觉得应该在新版本中更新调整。process.nextTick()vsPromiseprocess.nextTick()对应nextTickQueue,Promise对应microTaskQueue。这两个都不是事件循环的一部分,但是都是在当前某个操作之后执行的,那么这两个的执行顺序呢//process_nexttick_vs_promise.jsleti=0;functionfoo(){i+=1;如果(i>2)返回;console.log('foofunc');setTimeout(()=>{console.log('timeout');},0);承诺.解决()。然后(foo);}setTimeout(foo,0);Promise.resolve().then(()=>{console.log('promise');});process.nextTick(()=>{console.log('nexttick');});运行代码,结果如下:$nodeprocess_nexttick_vs_promise.jsnexttickpromisefoofuncfoofunctimeouttimeout如果你明白上面的输出是什么,那么你就可以掌握Nodejs中的事件循环了。参考资料TheNode.jsEventLoop,Timers,andprocess.nextTick()Node.jseventloopworkflow&lifecycleinlowleveldiscussion讨论原文地址欢迎大家一起讨论,有好的地方还请指正.
