下面是事件循环不同阶段的示意图:
┌───────────────────────┐┌─>│ timers │<————— 执行 setTimeout()、setInterval() 的回调│ └──────────┬────────────┘| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调│ ┌──────────┴────────────┐│ │ pending callbacks │<————— 执行由上一个 Tick 延迟下来的 I/O 回调(待完善,可忽略)│ └──────────┬────────────┘| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调│ ┌──────────┴────────────┐│ │ idle, prepare │<————— 内部调用(可忽略)│ └──────────┬────────────┘| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调| | ┌───────────────┐│ ┌──────────┴────────────┐ │ incoming: │ - (执行几乎所有的回调,除了 close callbacks 以及 timers 调度的回调和 setImmediate() 调度的回调,在恰当的时机将会阻塞在此阶段)│ │ poll │<─────┤ connections, ││ └──────────┬────────────┘ │ data, etc. ││ | | || | └───────────────┘| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调| ┌──────────┴────────────┐│ │ check │<————— setImmediate() 的回调将会在这个阶段执行│ └──────────┬────────────┘| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调│ ┌──────────┴────────────┐└──┤ close callbacks │<————— socket.on('close', ...)└───────────────────────┘
对比浏览器 想理解整个 loop 的过程,我们可以参照浏览器的 event loop,因为浏览器的比较简单,如下:
┌───────────────────────┐┌─>│ timers │<————— 执行一个 MacroTask Queue 的回调│ └──────────┬────────────┘| |<-- 执行所有 MicroTask Queue 的回调| ────────────┘
其实 nodejs 与浏览器的区别,就是 nodejs 的 MacroTask 分好几种,而这好几种又有不同的 task queue,而不同的 task queue 又有顺序区别,而 MicroTask 是穿插在每一种【注意不是每一个!】MacroTask 之间的。
其实图中已经画的很明白:
所以我们可以按照浏览器的经验得出一个结论:
先执行所有类型为 timers 的 MacroTask,然后执行所有的 MicroTask(注意 NextTick 要优先哦); 进入 poll 阶段,执行几乎所有 MacroTask,然后执行所有的 MicroTask; 再执行所有类型为 check 的 MacroTask,然后执行所有的 MicroTask; 再执行所有类型为 close callbacks 的 MacroTask,然后执行所有的 MicroTask; 至此,完成一个 Tick,回到 timers 阶段; …… 如此反复,无穷无尽……
代码重现,我们会发现 setTimeout 和 setImmediate 在 Node 环境下执行是靠“随缘法则”的。
比如说下面这段代码:
setTimeout(() => {console.log("setTimeout");}, 0);setImmediate(() => {console.log("setImmediate");});// 执行的结果是这样子的:setImmediatesetTimeout// 再次执行setTimeoutsetImmediate
为什么会这样子呢?
这里我们要根据前面的那个事件循环不同阶段的图解来说明一下:
首先进入的是 timers
阶段,如果我们的机器性能一般,那么进入 timers
阶段,一毫秒已经过去了(setTimeout(fn, 0)
等价于 setTimeout(fn, 1)
),那么 setTimeout 的回调会首先执行。
如果没有到一毫秒,那么在 timers 阶段的时候,下限时间没到,setTimeout 回调不执行,事件循环来到了 poll 阶段,这个时候队列为空,此时有代码被 setImmediate(),于是先执行了 setImmediate()的回调函数,之后在下一个事件循环再执行 setTimemout 的回调函数。
而我们在执行代码的时候,进入 timers 的时间延迟其实是随机的,并不是确定的,所以会出现两个函数执行顺序随机的情况。
那我们再来看一段代码:
var fs = require("fs");fs.readFile(__filename, () => {setTimeout(() => {console.log("timeout");}, 0);setImmediate(() => {console.log("immediate");});});
这里我们就会发现,setImmediate 永远先于 setTimeout 执行。
原因如下:
fs.readFile 的回调是在 poll 阶段执行的,当其回调执行完毕之后,poll 队列为空,而 setTimeout 入了 timers 的队列,此时有代码被 setImmediate(),于是事件循环先进入 check 阶段执行回调,之后在下一个事件循环再在 timers 阶段中执行有效回调。
同样的,这段代码也是一样的道理:
setTimeout(() => {setImmediate(() => {console.log("setImmediate");});setTimeout(() => {console.log("setTimeout");}, 0);}, 0);
以上的代码在 timers 阶段执行外部的 setTimeout 回调后,内层的 setTimeout 和 setImmediate 入队,之后事件循环继续往后面的阶段走,走到 poll 阶段的时候发现队列为空,此时有代码被 setImmedate(),所以直接进入 check 阶段执行响应回调(注意这里没有去检测 timers 队列中是否有成员到达下限事件,因为 setImmediate()优先)。之后在第二个事件循环的 timers 阶段中再去执行相应的回调。
综上,我们可以总结:
如果两者都在主模块中调用,那么执行先后取决于进程性能,也就是随机。 如果两者都不在主模块调用(被一个异步操作包裹),那么 setImmediate 的回调永远先执行。
对于这两个,我们可以把它们理解成一个微任务。也就是说,它其实不属于事件循环的一部分。
那么他们是在什么时候执行呢?
不管在什么地方调用,他们都会在其所处的事件循环最后,事件循环进入下一个循环的阶段前执行。
举个?:
setTimeout(() => {console.log("timeout0");process.nextTick(() => {console.log("nextTick1");process.nextTick(() => {console.log("nextTick2");});});process.nextTick(() => {console.log("nextTick3");});console.log("sync");setTimeout(() => {console.log("timeout2");}, 0);}, 0);
结果是:
再解释一下:
timers 阶段执行外层 setTimeout 的回调,遇到同步代码先执行,也就有 timeout0、sync 的输出。遇到 process.nextTick 后入微任务队列,依次 nextTick1、nextTick3、nextTick2 入队后出队输出。之后,在下一个事件循环的 timers 阶段,执行 setTimeout 回调输出 timeout2。
最后 下面给出两段代码,如果能够理解其执行顺序说明你已经理解透彻。
代码 1:
setImmediate(function() {console.log("setImmediate");setImmediate(function() {console.log("嵌套setImmediate");});process.nextTick(function() {console.log("nextTick");});});// setImmediate// nextTick// 嵌套setImmediate
解析:事件循环 check 阶段执行回调函数输出 setImmediate,之后输出 nextTick。嵌套的 setImmediate 在下一个事件循环的 check 阶段执行回调输出嵌套的 setImmediate。
代码 2:
var fs = require("fs");function someAsyncOperation(callback) {// 假设这个任务要消耗 95msfs.readFile("/path/to/file", callback);}var timeoutScheduled = Date.now();setTimeout(function() {var delay = Date.now() - timeoutScheduled;console.log(delay + "ms have passed since I was scheduled");}, 100);// someAsyncOperation要消耗 95 ms 才能完成someAsyncOperation(function() {var startCallback = Date.now();// 消耗 10ms...while (Date.now() - startCallback < 10) {// do nothing}});
解析:事件循环进入 poll 阶段发现队列为空,并且没有代码被 setImmediate()。于是在 poll 阶段等待 timers 下限时间到达。当等到 95ms 时,fs.readFile 首先执行了,它的回调被添加进 poll 队列并同步执行,耗时 10ms。此时总共时间累积 105ms。等到 poll 队列为空的时候,事件循环会查看最近到达的 timer 的下限时间,发现已经到达,再回到 timers 阶段,执行 timer 的回调。
poll 阶段主要有两个功能:
poll 阶段用于获取并执行几乎所有 I/O 事件回调,是使得 node event loop 得以无限循环下去的重要阶段。所以它的首要任务就是同步执行所有 poll queue 中的所有 callbacks 直到 queue 被清空或者已执行的 callbacks 达到一定上限,然后结束 poll 阶段,接下来会有几种情况: