在以往的Node版本中,也就是11.0 之前, JS的执行栈的顺序是
执行同类型的所有宏任务 -> 在间隙时间执行微任务 ->event loop 完毕执行下一个event loop
而在最新版本的11.0之后, NodeJS为了向浏览器靠齐,对底部进行了修改,最新的执行栈顺序和浏览器的执行栈顺序已经是一样了
执行首个宏任务 -> 执行宏任务中的微任务 -> event loop执行完毕执行下一个eventloop
但是这里是有问题的,虽然NodeJS的执行栈顺序向浏览器靠齐了, 但这是一种退步, 在整个JS的运行所消耗的时间上,是比之前的版本慢了一些,所以建议在项目中,还是使用11.0之前的版本,在12.0 或者之后的版本,Node官方将收尾工作 完成后, 再建议使用11.0之后的版本
Node.js的Event Loop过程:
- 执行全局Script的同步代码
- 执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务
- 开始执行macrotask宏任务,共6个阶段,从第1个阶段开始执行相应每一个阶段macrotask中的所有任务,注意,这里是所有每个阶段宏任务队列的所有任务,在浏览器的Event Loop中是只取宏队列的第一个任务出来执行,每一个阶段的macrotask任务执行完毕后,开始执行微任务,也就是步骤2
- Timers Queue -> 步骤2 -> I/O Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue …
下面这个图这就是Node的Event Loop【简化版】
NodeJS中 主要关注的宏任务队列有4个(Timers阶段、IO操作、immediates、close关闭操作),在这之中有两个微任务队列: nexttick、other micro tasks。
浏览器和Node端有什么不同?
- 浏览器的Event Loop和Node.js中的EventLoop是不同的,实现机制也不一样,不要混文一谈。
- Node.js中可以理解为4个宏任务队列和2个微任务队列,但是执行宏任务时有6个阶段。
- Node.js中,先执行全局Script代码,执行完同步代码调用栈清空后,先从微任务队列Next Tick Queue中依次取出所有的任务放入调用栈中执行,再从微任务队列Other Microtask Queue依次取出所有的任务放入调用栈中执行。然后开始宏任务的6个阶段,每个阶段都将该宏任务队列中的所有任务都取出来执行, 在执行下一阶段宏任务,以此来构成事件循环。
- MacroTask(宏任务)包括:setTimeout、setInterval、setImmediate(Node)、 requestAnimation(浏览器)、IO、UIrendering
- Microtask包括:process.nextTick(Node)、 Promise.then、Object.observe、MutationObserver
注意:new Promise() 是构造函数里面的同步代码,而非微任务!
问题:微任务中的nextTick和then谁运行的快?
Promise.resolve('123').then(res=>{ console.log(res)})
process.nextTick(() => console.log('nextTick'))
代码经过运行之后可以看出nextTick 是比普通的微任务快一些的,这是为什么呢? 可以看看我们上面的那个NodeJS中的任务队列图, nextTick的microtask queue是优先于 promise的microtask queue。
setTimeout 和 setImmediate的问题
首先来看一段代码
setTimeout(() => {
console.log('setTimeout')
}, 0);
setImmediate(() => {
console.log('setImmediate')
})
运行结果:
为什么结果不确定呢?
setTimeout/setInterval 的第二个参数取值范围是:[1, 2^31 - 1],如果超过这个范围则会初始化为 1,即 setTimeout(fn, 0) === setTimeout(fn, 1)。
我们知道 setTimeout 的回调函数在 timer 阶段执行,setImmediate 的回调函数在 check 阶段执行,event loop 的开始会先检查 timer 阶段,但是在开始之前到 timer 阶段会消耗一定时间;
所以就会出现两种情况:
- timer 前的准备时间超过 1ms,满足 loop->time >= 1,则执行 timer 阶段(setTimeout)的回调函数
- timer 前的准备时间小于 1ms,则先执行 check 阶段(setImmediate)的回调函数,下一次 event loop 执行 timer 阶段(setTimeout)的回调函数。
如果时间大于10ms 那么一定先执行timer阶段的setTimeout
(如果setTimeout和setImmediate是几乎同一个时间执行的话,看setTimeout的准备阶段大不大,如果大的话,首先执行setTimeout 如果不大, 在看setTimeout的 延迟时间,如果小于1 , 最后的结果不稳定,如果大于1最后结果,先执行setImmediate
)
问题来了如何一定先执行setImmediate呢?
我们可以利用IO文件操作,来直接跳过Node的timers阶段
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
})
这样的话,是在操作文件的流程中 运行的setTimeout、setImmediate, 所以已经太过了timers阶段,那么是从第二个阶段IO开始的, 所以一定会先执行setImmediate阶段
测试代码(巩固提升)
console.time("start")
setTimeout(function () {
console.log(2);
}, 10);
setImmediate(function () {
console.log(1);
});
new Promise(function (resolve) {
console.log(3);
resolve();
console.log(4);
}).then(function () {
console.log(5);
console.timeEnd("start")
});
console.log(6);
process.nextTick(function () {
console.log(7);
});
console.log(8);