You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
varfs=require('fs');functionsomeAsyncOperation(callback){// 假设这个任务要消耗 95msfs.readFile('/path/to/file',callback);}vartimeoutScheduled=Date.now();setTimeout(function(){vardelay=Date.now()-timeoutScheduled;console.log(delay+"ms have passed since I was scheduled");},100);// someAsyncOperation要消耗 95 ms 才能完成someAsyncOperation(function(){varstartCallback=Date.now();// 消耗 10ms...while(Date.now()-startCallback<10){;// do nothing}});
问题引入
接触过事件循环的同学大都会纠结一个点,就是在Node中
setTimeout
和setImmediate
执行顺序的随机性。比如说下面这段代码:
执行的结果是这样子的:
为什么会出现这种情况呢?别急,我们先往下看。
浏览器中事件循环模型
我们都知道,JavaScript是单线程的语言,对
I/O
的控制是通过异步来实现的,具体是通过“事件循环”机制来实现。对于JavaScript中的单线程,指的是JavaScript执行在单线程中,而内部
I/O
任务其实是另有线程池来完成的。在浏览器中,我们讨论事件循环,是以“从宏任务队列中取一个任务执行,再取出微任务队列中的所有任务”来分析执行代码的。但是在Node环境中并不适用。具体的浏览器事件循环解析:传送门
在Node中,事件循环的模型和浏览器相比大致相同,而最大的不同点在于Node中事件循环分不同的阶段。具体我们下面会讨论到。本文的核心也在这里。
Node中事件循环阶段解析
下面是事件循环不同阶段的示意图:
每个阶段都有一个先进先出的回调队列要执行。而每个阶段都有自己的特殊之处。简单来说,就是当事件循环进入某个阶段后,会执行该阶段特定的任意操作,然后才会执行这个阶段里的回调。当队列被执行完,或者执行的回调数量达到上限后,事件循环才会进入下一个阶段。
以下是各个阶段详情。
timers
一个
timer
指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定的时间过后,timers
会尽早的执行回调,但是系统调度或者其他回调的执行可能会延迟它们。下限的时间有一个范围:
[1, 2147483647]
,如果设定的时间不在这个范围,将被设置为1。I/O callbacks
这个阶段执行一些系统操作的回调,比如说TCP连接发生错误。
idle, prepare
系统内部的一些调用。
poll
这是最复杂的一个阶段。
poll
阶段有两个主要的功能:一是执行下限时间已经达到的timers
的回调,一是处理poll
队列里的事件。注:Node很多API都是基于事件订阅完成的,这些API的回调应该都在
poll
阶段完成。以下是Node官网的介绍:
笔者把官网陈述的情况以不同的条件分解,更加的清楚。(如果有误,师请改正。)
当事件循环进入
poll
阶段:poll
队列不为空的时候,事件循环肯定是先遍历队列并同步执行回调,直到队列清空或执行回调数达到系统上限。poll
队列为空的时候,这里有两种情况。setImmediate()
设定了回调,那么事件循环直接结束poll
阶段进入check
阶段来执行check
队列里的回调。setImmediate()
设定回调:timers
,那么此时事件循环会检查timers
,如果有一个或多个timers
下限时间已经到达,那么事件循环将绕回timers
阶段,并执行timers
的有效回调队列。timers
,这个时候事件循环是阻塞在poll
阶段等待回调被加入poll
队列。check
这个阶段允许在
poll
阶段结束后立即执行回调。如果poll
阶段空闲,并且有被setImmediate()
设定的回调,那么事件循环直接跳到check
执行而不是阻塞在poll
阶段等待回调被加入。setImmediate()
实际上是一个特殊的timer
,跑在事件循环中的一个独立的阶段。它使用libuv
的API
来设定在poll
阶段结束后立即执行回调。注:
setImmediate()
具有最高优先级,只要poll
队列为空,代码被setImmediate()
,无论是否有timers
达到下限时间,setImmediate()
的代码都先执行。close callbacks
如果一个
socket
或handle
被突然关掉(比如socket.destroy()
),close
事件将在这个阶段被触发,否则将通过process.nextTick()
触发。关于setTimeout和setImmediate
代码重现,我们会发现
setTimeout
和setImmediate
在Node环境下执行是靠“随缘法则”的。比如说下面这段代码:
执行的结果是这样子的:
为什么会这样子呢?
这里我们要根据前面的那个事件循环不同阶段的图解来说明一下:
首先进入的是
timers
阶段,如果我们的机器性能一般,那么进入timers
阶段,一毫秒已经过去了(setTimeout(fn, 0)
等价于setTimeout(fn, 1)
),那么setTimeout
的回调会首先执行。如果没有到一毫秒,那么在
timers
阶段的时候,下限时间没到,setTimeout
回调不执行,事件循环来到了poll
阶段,这个时候队列为空,此时有代码被setImmediate()
,于是先执行了setImmediate()
的回调函数,之后在下一个事件循环再执行setTimemout
的回调函数。而我们在执行代码的时候,进入
timers
的时间延迟其实是随机的,并不是确定的,所以会出现两个函数执行顺序随机的情况。那我们再来看一段代码:
这里我们就会发现,
setImmediate
永远先于setTimeout
执行。原因如下:
fs.readFile
的回调是在poll
阶段执行的,当其回调执行完毕之后,poll
队列为空,而setTimeout
入了timers
的队列,此时有代码被setImmediate()
,于是事件循环先进入check
阶段执行回调,之后在下一个事件循环再在timers
阶段中执行有效回调。同样的,这段代码也是一样的道理:
以上的代码在
timers
阶段执行外部的setTimeout
回调后,内层的setTimeout
和setImmediate
入队,之后事件循环继续往后面的阶段走,走到poll
阶段的时候发现队列为空,此时有代码被setImmedate()
,所以直接进入check
阶段执行响应回调(注意这里没有去检测timers
队列中是否有成员到达下限事件,因为setImmediate()
优先)。之后在第二个事件循环的timers
阶段中再去执行相应的回调。综上,我们可以总结:
setImmediate
的回调永远先执行。process.nextTick() and Promise
对于这两个,我们可以把它们理解成一个微任务。也就是说,它其实不属于事件循环的一部分。
那么他们是在什么时候执行呢?
不管在什么地方调用,他们都会在其所处的事件循环最后,事件循环进入下一个循环的阶段前执行。
举个🌰:
结果是:
再解释一下:
timers
阶段执行外层setTimeout
的回调,遇到同步代码先执行,也就有timeout0
、sync
的输出。遇到process.nextTick
后入微任务队列,依次nextTick1
、nextTick3
、nextTick2
入队后出队输出。之后,在下一个事件循环的timers
阶段,执行setTimeout
回调输出timeout2
。最后
下面给出两段代码,如果能够理解其执行顺序说明你已经理解透彻。
代码1:
解析:事件循环
check
阶段执行回调函数输出setImmediate
,之后输出nextTick
。嵌套的setImmediate
在下一个事件循环的check
阶段执行回调输出嵌套的setImmediate
。代码2:
解析:事件循环进入
poll
阶段发现队列为空,并且没有代码被setImmediate()
。于是在poll
阶段等待timers
下限时间到达。当等到95ms
时,fs.readFile
首先执行了,它的回调被添加进poll
队列并同步执行,耗时10ms
。此时总共时间累积105ms
。等到poll
队列为空的时候,事件循环会查看最近到达的timer
的下限时间,发现已经到达,再回到timers
阶段,执行timer
的回调。如果有什么问题,欢迎留言交流探讨。
参考链接:
https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
creeperyang/blog#26
The text was updated successfully, but these errors were encountered: