问题介绍。接触过事件循环的同学都会纠结一点,就是Node中setTimeout和setImmediate执行顺序的随机性。例如下面的代码:setTimeout(()=>{console.log('setTimeout');},0);setImmediate(()=>{console.log('setImmediate');})执行结果是这样的:为什么会这样?别着急,我们先往下看。我们都知道浏览器中的事件循环模型。JavaScript是单线程语言,对I/O的控制是通过异步实现的,具体是通过“事件循环”机制实现的。对于JavaScript中的单线程,是指JavaScript在单线程中执行,内部I/O任务实际上是由另一个线程池完成的。在浏览器中,我们在讨论事件循环时,通过“从宏任务队列中取出一个任务执行,然后取出微任务队列中的所有任务”来分析执行代码。但它不适用于Node环境。具体浏览器事件循环分析:传送门在Node中,事件循环模型与浏??览器大致相同,但最大的区别在于Node中的事件循环分为不同的阶段。具体来说,我们将在下面讨论。本文的核心也在这里。下面是事件循环不同阶段的示意图:每个阶段都有一个先进先出的回调队列去执行。每个阶段都有自己的特点。简单来说,当事件循环进入到某个阶段时,会执行该阶段特有的任何操作,然后执行该阶段的回调。当队列耗尽,或者执行的回调数达到上限时,事件循环会进入下一阶段。以下是每个阶段的详细信息。timers定时器指定一个下限时间而不是确切的时间,到达下限时间后执行回调。定时器在指定时间过去后尽可能早地执行回调,但系统调度或其他回调执行可能会延迟它们。从技术上讲,poll阶段控制timer何时执行,具体执行位置在timer中。下限时间有一个范围:[1,2147483647],如果设置的时间不在这个范围内,则设置为1。I/O回调这个阶段执行一些系统操作的回调,比如TCP连接错误。空闲,准备一些系统内部的调用。poll这是最复杂的阶段。poll阶段主要有两个作用:一个是执行最小时间到达定时器的回调,另一个是处理poll队列中的事件。注意:Node的很多API都是基于事件订阅完成的,这些API的回调应该在poll阶段完成。以下是Node官网的介绍:笔者将官网所表述的情况按照不同的条件进行了分解,使其更加清晰。(如有错误,请指正)当事件循环进入轮询阶段:当轮询队列不为空时,事件循环要先遍历队列,同步执行回调,直到队列被清空或轮询次数达到执行的回调达到系统上限。当轮询队列为空时,有两种情况。如果代码已经通过setImmediate()设置了回调,那么事件循环直接结束poll阶段,进入check阶段执行check队列中的回调。如果代码没有设置setImmediate()设置回调:如果设置了定时器,那么事件循环会在此时检查定时器,如果一个或多个定时器的下限时间已经到达,那么事件循环会回绕定时器阶段,并执行定时器的有效回调队列。如果没有设置定时器,事件循环会阻塞在轮询阶段,等待此时将回调加入轮询队列。检查阶段允许在轮询阶段结束后立即执行回调。如果poll阶段空闲,并且有setImmediate()设置的回调,那么事件循环直接跳转到检查执行,而不是阻塞在poll阶段等待回调加入。setImmediate()实际上是一个特殊的计时器,它在事件循环的一个单独阶段运行。它使用libuv的API将回调设置为在轮询阶段结束后立即执行。注意:setImmediate()具有最高优先级。只要轮询队列为空,代码就会由setImmediate()执行。不管有没有定时器到达下限时间,setImmediate()的代码都会先执行。closecallbacks如果一个socket或者handle突然关闭(比如socket.destroy()),这个阶段会触发close事件,否则会被process.nextTick()触发。关于setTimeout和setImmediate的代码复现,我们会发现在Node环境下setTimeout和setImmediate的执行是基于“随机规则”的。例如下面的代码:setTimeout(()=>{console.log('setTimeout');},0);setImmediate(()=>{console.log('setImmediate');})执行结果是这样的:为什么会这样?这里我们要根据之前事件循环不同阶段的图来说明一下:首先进入timers阶段,如果我们的机器性能一般,那么进入timers阶段,过了一毫秒(setTimeout(fn,0)是相当于setTimeout(fn,1)),那么setTimeout回调会先执行。如果没有达到一毫秒,那么在timers阶段,还没有达到下限时间,不执行setTimeout回调,事件循环进入poll阶段。这时候,队列是空的。这时候有些代码是setImmediate(),所以先执行setImmediate。()回调函数,然后在下一个事件循环中执行setTimemout的回调函数。我们在执行代码的时候,进入定时器的时间延迟其实是随机的,并不是确定性的,所以会出现两个函数执行顺序随机的情况。然后我们再看一段代码:varfs=require('fs')fs.readFile(__filename,()=>{setTimeout(()=>{console.log('timeout');},0);setImmediate(()=>{console.log('immediate');});});这里我们会发现setImmediate总是在setTimeout之前执行。原因如下:fs.readFile的回调是在poll阶段执行的。callback执行后,poll队列为空,setTimeout进入定时器队列。这时候有一段代码是setImmediate(),所以事件循环先进入check。在stage中执行回调,然后在下一个事件循环的timers阶段执行有效的回调。类似地,这段代码是相同的:},0);},0);上面代码在timers阶段执行完外部setTimeout回调后,将内部setTimeout和setImmediate入队,然后事件循环继续下一阶段,到达poll阶段,此时发现队列为空。这时候有一段代码是setImmedate(),所以直接进入check阶段执行response回调(注意这里没有检测timers队列中是否有成员到达下限事件,因为setImmediate()取优先事项)。然后在第二个事件循环的timers阶段执行相应的回调。综上所述,我们可以得出结论:如果两者都在主模块中调用,那么执行顺序取决于进程的性能,即随机。如果两者都没有在主模块中调用(由异步操作包装),那么setImmediate回调将始终首先执行。process.nextTick()和Promise对于这两个,我们可以理解为一个微任务。也就是说,它实际上并不是事件循环的一部分。那么他们什么时候被处决呢?无论它们在哪里被调用,它们都会在它们所在的事件循环结束时被执行,在事件循环进入下一个循环阶段之前。例如?:setTimeout(()=>{console.log('timeout0');process.nextTick(()=>{console.log('nextTick1');process.nextTick(()=>{console.log('nextTick2');});});process.nextTick(()=>{console.log('nextTick3');});console.log('sync');setTimeout(()=>{控制台.log('timeout2');},0);},0);结果是:再解释一下:timers阶段执行外层setTimeout的回调,先执行同步代码,有timeout0和sync的输出。遇到process.nextTick后,进入microtask队列,然后nextTick1,nextTick3,nextTick2进入队列,然后退出队列输出。之后,在下一个事件循环的timers阶段,执行setTimeout回调输出timeout2。最后,下面给出下面两段代码。如果你能理解执行顺序,你就理解透彻了。代码1:setImmediate(function(){console.log("setImmediate");setImmediate(function(){console.log("nestedsetImmediate");});process.nextTick(function(){console.log("nextTick");})});//setImmediate//nextTick//嵌套setImmediate分析:事件循环检查阶段执行回调函数输出setImmediate,然后输出nextTick。嵌套的setImmediate在下一个事件循环的check阶段执行回调输出嵌套的setImmediate。代码2:varfs=require('fs');functionsomeAsyncOperation(callback){//假设此任务将消耗95msfs.readFile('/path/to/file',callback);}vartimeoutScheduled=Date.now();setTimeout(function(){vardelay=Date.now()-timeoutScheduled;console.log(delay+"mshavepassedsinceIwasscheduled");},100);//someAsyncOperation需要95毫秒才能完成someAsyncOperation(function(){varstartCallback=Date.now();//消耗10ms...while(Date.now()-startCallback<10){;//什么都不做}});分析:事件循环进入poll阶段发现队列为空,没有代码setImmediate()。所以在轮询阶段等待定时器的下限到达。等待95ms时,先执行fs.readFile,将其回调加入poll队列同步执行,耗时10ms。此时累计总时间为105ms。当轮询队列为空时,事件循环会检查最近到达的定时器的下限时间,发现已经到达,则返回定时器阶段,执行定时器的回调。如有任何问题,欢迎留言交流讨论。参考链接:https://nodejs.org/en/docs/gu...https://github.com/creeperyan...
