事件循环,即 Event Loop
,其实就是 JS 管理事件执行的一个流程,具体的管理方法由 JS 运行的环境决定,目前 JS 的主要运行环境有浏览器和 Node。javascript
浏览器和 Node 的事件循环,都是先初始化一个循环,执行同步代码,遇到异步操做时,会将其交给对应的线程处理,主线程则继续往下执行,异步操做执行完毕后,对应的 callback 回调会被推入事件队列,并在合适的时机执行。每执行一次循环体的过程,咱们称之为一个 Tick。html
与浏览器不一样的是,Node 的循环分为几个阶段,每一个阶段分别处理不一样的事件,而浏览器的循环不存在这样的阶段划分。下面咱们介绍一下 Node 事件循环的几个阶段。java
Node 的事件循环分为几个阶段,以下图(除了 incoming,每个方框表明一个阶段):node
咱们先简单看看每一个阶段的做用,也就是在一个 Tick 中,Node 是按照怎样的顺序工做的:npm
setTimeout
、setInterval
的回调;setImmediate
的回调;socket.on('close', ...)
每一个阶段都对应一个 FIFO 的队列,循环进入某个阶段后,只有在两种状况下会跳出该阶段进入下一个阶段,一是将其队列中的回调所有执行完,二是执行数达到了该阶段的上限。api
接下来咱们重点看一下 poll 阶段,poll 阶段主要作两件事情:浏览器
maxPollTime
;如下为 poll 阶段的流程:markdown
setImmediate
回调;maxPollTime
)。注意:poll 阶段空闲,即 poll 队列为空的时候,一旦有新的 setImmediate
回调,循环就会结束 poll 进入 check 阶段;或者一旦有新的 timer 计时结束,循环就会绕回 Timers 阶段;若是同时出现二者的回调,setImmediate
的优先级更高并发
首先明确 setImmediate
和 setTimeout
各自的执行时机:setImmediate
是一个特殊的定时器,它的回调会在 check 阶段执行,也就是紧跟在 poll 阶段后面执行;setTimeout
的回调会在达到 delay 时间后,尽快执行,前提是计时结束并把回调推入了 Timers 的队列。 另外还需明确一点,事件循环除了可以正常进入 Timers 阶段外,poll 阶段一旦空闲而且没有待执行的 setImmediate
回调,就会去检查是否有计时结束的 timer,若是有的话,就会绕回到 Timers 阶段,因此 setTimeout
回调的执行时机实际上有两个。less
好了,咱们如今来看 setImmediate
和 setTimeout(fn, 0)
回调的执行顺序,能够分为如下两种状况:
setImmediate
的回调会先于 setTimeout(fn, 0) 执行如下示例中,咱们都假定 setImmediate
的回调为 cb_immediate
,setTimeout(fn, 0)
的回调为 cb_timeout
。
代码以下:
// test_timer.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
复制代码
咱们屡次运行会发现,“immediate”始终会在“timeout”以前打印:
$ node test_timer.js
immediate
timeout
复制代码
下面咱们分析一下 cb_immediate
始终先执行的缘由。 首先,I/O 操做的回调在 poll 队列里,是在 poll 阶段执行的。前面咱们提到,poll 阶段一旦空闲,就会检查是否有待执行的 setImmediate
回调,若是有,就会结束等待进入 check 阶段,因此即便这时有新的 timer 计时结束,也要等到 check、close 都结束并进入新的 Tick 才能执行。poll 检查发现 cb_immediate
待执行,因此循环直接进入 check 阶段执行 cb_immediate
,而 cb_timeout
最快也要在下一个 Tick 才会被执行。
代码以下:
setTimeout(() => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {1
console.log('immediate');
});
}, 0);
复制代码
屡次运行后咱们会发现,“immediate”始终比“timeout”先打印。 外层 setTimeout
的回调是在 Timers 阶段执行的,执行完回调后,事件循环继续日后面走,走到 poll 阶段后,一旦 poll 空闲,就会检查并发现有待执行的 setImmediate
回调,发现存在 cb_immediate
,因而循环直接进入 check 阶段,率先执行 cb_immediate
。
综合上面两个示例,咱们能够看出,在同一个异步操做的回调中同时调用 setImmediate
和 setTimeout(fn, 0)
,setImmediate
之因此始终会先执行,正是因为计时器一旦错过了 Timers 阶段,下一个执行回调的时机在 poll 阶段,而 poll 阶段检查过程当中,setImmediate
的优先级高于 setTimeout
,一旦发现有待执行的 setImmediate
回调,循环就会继续往下走,因此 setImmediate
回调的执行永远先于 setTimeout
回调。
咱们接着看一下在主模块同时调用 setImmediate
和 setTimeout(fn, 0)
的状况。
代码以下:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {1
console.log('immediate');
});
复制代码
运行以上代码,咱们会发现,时而先打印出“timeout”,时而先打印出“immediate”,即二者的执行顺序是不肯定的。
咱们先理一下主要流程:
setTimeout
交给定时器线程处理;setImmediate
,即调用 libuv 提供的一个 API,该 API 会将 cb_immediate
放入 check 阶段的队列;cb_timeout
),若为空,继续往下执行;setImmediate
回调(即 cb_immediate
),进入 check 阶段;cb_Immediate
根据以上流程能够看出,cb_immediate
和 cb_timeout
的执行顺序,就取决于第 e 步中 Timers 的队列是否已经存在 cb_timeout
,若是存在,则 cb_timeout
先执行,不然 cb_immediate
先执行。
这里补充一点,在 Node 中,setTimeout(fn, 0)
会被强制改成 setTimeout(fn, 1)
,这一点在官方文档中有相关说明:
When delay is larger than 2147483647 or less than 1, the delay will be set to 1.
若是循环进入 Timers 阶段的时候,距离 setTimeout
执行已通过去了 1ms,而且 cb_timeout
已经被推入 Timers 的队列,那么循环就会取出 cb_timeout
并执行;反之,若是循环进入 Timers 阶段的时候,cb_timeout
尚未在队列内,那么 cb_timeout
就不会在这个 Tick 被执行,cb_immediate
会先执行。
而循环进入 Timers 阶段的时候,是否已经通过了 1ms,会受到多方面的影响,包括同步代码执行所花费的时间,以及系统的性能,机器的状态差别也会致使每次运行的结果不一样。因此同时在主模块调用 setImmediate
和 setTimeout(fn, 0)
,两个回调的执行顺序是不肯定的。
你们可能会发现,咱们在介绍事件循环的阶段时,process.nextTick()
没有出如今任何一个阶段。这是由于 process.nextTick()
不属于任何一个阶段,事实上,它是在 “阶段之间” 执行的。 在任何一个阶段调用了 process.nextTick()
,nextTick 的回调都会在当前阶段结束后当即执行,全部回调执行完后,事件循环才进入下一个阶段,因此过多的或长时间执行的 nextTick 回调,实际上是能够阻塞整个事件循环的,因此得当心使用 nextTick。
咱们回顾一下二者的回调的执行时机,process.nextTick()
的回调是在当前阶段结束后就当即执行,setImmediate()
的回调是在每一个循环的 check 阶段执行。细心的读者就会发现,process.nextTick()
彷佛比 setImmediate()
更“immediate”,更即时。 事实上官方也提到了这一点,二者的名字应该倒过来才比较合理,但修更名字的话,影响范围太大了,npm 上全部用到这两个方法的包都会受到影响,因此即便二者的名字存在必定的迷惑性,可是目前看来,更名是不现实也是不可能的。
二者有如下两点区别:
和浏览器同样,Node 也会维护一个微任务列表。和浏览器不一样的是,浏览器是在宏任务即将结束的时候,检查宏任务对应的微任务列表是否为空,若不为空,则执行全部微任务后,再进入下一个 Tick。而 Node 的微任务执行时机是在各个阶段之间,一个阶段结束后,事件循环会去检查微任务列表是否为空,若不为空,则执行完全部微任务后,才进入循环的下一个阶段。 注意,setImmediate
是 Node 特有的宏任务,process.nextTick
是 Node 特有的微任务。