setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop (4)

但是请注意我们上面console.log('setTimeout')和console.log('setImmediate')都包在了一个setTimeout里面,如果直接写在最外层会怎么样呢?代码改写如下:

console.log('outer'); setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); });

我们来运行下看看效果:

image-20200322214105295

好像是setTimeout先输出来,我们多运行几次看看:

image-20200322214148090

怎么setImmediate又先出来了,这代码是见鬼了还是啥?这个世界上是没有鬼怪的,所以事情都有原因的,我们顺着之前的Event Loop再来理一下。在理之前,需要告诉大家一件事情,node.js里面setTimeout(fn, 0)会被强制改为setTimeout(fn, 1),。(说到这里顺便提下,HTML 5里面setTimeout最小的时间限制是4ms)。原理我们都有了,我们来理一下流程:

外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段

遇到setTimeout,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times阶段

遇到setImmediate塞入check阶段

同步代码执行完毕,进入Event Loop

先进入times阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳过

跳过空的阶段,进入check阶段,执行setImmediate回调

通过上述流程的梳理,我们发现关键就在这个1毫秒,如果同步代码执行时间较长,进入Event Loop的时候1毫秒已经过了,setTimeout执行,如果1毫秒还没到,就先执行了setImmediate。每次我们运行脚本时,机器状态可能不一样,导致运行时有1毫秒的差距,一会儿setTimeout先执行,一会儿setImmediate先执行。但是这种情况只会发生在还没进入timers阶段的时候。像我们第一个例子那样,因为已经在timers阶段,所以里面的setTimeout只能等下个循环了,所以setImmediate肯定先执行。同理的还有其他poll阶段的API也是这样的,比如:

var fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); }); });

这里setTimeout和setImmediate在readFile的回调里面,由于readFile回调是I/O操作,他本身就在poll阶段,所以他里面的定时器只能进入下个timers阶段,但是setImmediate却可以在接下来的check阶段运行,所以setImmediate肯定先运行,他运行完后,去检查timers,才会运行setTimeout。

类似的,我们再来看一段代码,如果他们两个不是在最外层,而是在setImmediate的回调里面,其实情况跟外层一样,结果也是随缘的,看下面代码:

console.log('outer'); setImmediate(() => { setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); }); });

原因跟写在最外层差不多,因为setImmediate已经在check阶段了,里面的循环会从timers阶段开始,会先看setTimeout的回调,如果这时候已经过了1毫秒,就执行他,如果没过就执行setImmediate。

process.nextTick()

process.nextTick()是一个特殊的异步API,他不属于任何的Event Loop阶段。事实上Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick(),这个执行完后才会继续Event Loop。我们写个例子来看下:

var fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); process.nextTick(() => { console.log('nextTick 2'); }); }); process.nextTick(() => { console.log('nextTick 1'); }); });

这段代码的打印如下:

image-20200322221221927

我们还是来理一下流程:

我们代码基本都在readFile回调里面,他自己执行时,已经在poll阶段

遇到setTimeout(fn, 0),其实是setTimeout(fn, 1),塞入后面的timers阶段

遇到setImmediate,塞入后面的check阶段

遇到nextTick,立马执行,输出'nextTick 1'

到了check阶段,输出'setImmediate',又遇到个nextTick,立马输出'nextTick 2'

到了下个timers阶段,输出'setTimeout'

这种机制其实类似于我们前面讲的微任务,但是并不完全一样,比如同时有nextTick和Promise的时候,肯定是nextTick先执行,原因是nextTick的队列比Promise队列优先级更高。来看个例子:

const promise = Promise.resolve() setImmediate(() => { console.log('setImmediate'); }); promise.then(()=>{ console.log('promise') }) process.nextTick(()=>{ console.log('nextTick') })

代码运行结果如下:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wppszw.html