小柒前面总结了几篇关于浏览器的事件循环,这篇文章主要总结Node的事件循环。
Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。
而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现。
libuv引擎中的事件循环分为6个阶段,它们会按顺序反复执行。每当进入某个阶段,都会从对应的回调队列中取出函数执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
从图中可以看出Node中事件循环的顺序:
外部输入数据–>轮询阶段(poll)–>检查阶段(check)–>关闭事件回调阶段(close callback)–>定时器检测阶段(timer)–>I/O事件回调阶段(I/O callbacks)–>闲置阶段(idle, prepare)–>轮询阶段(按照该顺序反复运行)…
timers阶段: 执行timer(setTimeout 、setInterval)的回调
I/O callbacks阶段: 处理一些上一轮循环中少数未执行的I/O回调
idle,prepare阶段: 仅Node内部使用
poll阶段: 获取新的I/O事件,适当的条件下node将阻塞在这里。(发生阻塞的情况为:poll队列为空,且没有代码设定为setImmediate())
check阶段: 执行setImmediate()的回调。(如果poll阶段空闲,并且有被setImmediate()设定的回调,那么事件循环直接跳到check执行,而不是阻塞在poll阶段等待回调被加入)
[注意]: setImmediate()在这个阶段具有最高优先级,只要poll队列为空,代码被setImmediate(),无论是否有timers达到下限时间,setImmediate()的代码都先执行。 (下面有例子会解释)
close callbacks阶段:执行socket的close事件回调
[注意]: 以上阶段不包括process.nextTick()。日常开发中的绝大部分异步任务都是在timers、poll、check这3个阶段处理的
Node端事件循环中的异步队列也是这两种:macro(宏任务)队列和 micro(微任务)队列。
常见的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作等。
常见的 micro-task 比如: process.nextTick、new Promise().then(回调)、mutationObserver等。
两者非常相似,区别在于调用时机不同:
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
这段代码中setTimeout可能执行在前,也可能执行在后(在node中是“随缘的”)。原因:
而我们在执行代码的时候,进入timers的时间延迟其实是随机的,并不是确定的,所以会出现两个函数执行顺序随机的情况。
再来看一段代码:
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
我们会发现,setImmediate永远先于setTimeout执行。
原因如下:
下面的代码也是同样的道理:
setTimeout(() => {
setImmediate(() => {
console.log('setImmediate');
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
}, 0);
在timers阶段执行外部的setTimeout之后,内层setTimeout与setImmediate入队,时间循环继续往下走,到poll阶段发现队列为空,此时代码有setImmediate(),所以直接进入check阶段执行setimmediate()的回调。之后再第二次时间循环的timers中再执行相应的回调。
总结:
这是一个微任务,node的事件循环不包括process.nextTick() 。 它有自己的队列,当事件循环的每一个阶段完成之后,如果存在nextTick队列,就会清空队列中的所有回调函数,并且优先于其他microtask执行。
例1:
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
})
})
})
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
例2:
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);
// timeout0 -> sync->nextTick1->nextTick3->nextTick2->timeout2
解释:
timers阶段执行外层setTimeout的回调,遇到同步代码先执行,也就有timeout0、sync的输出。遇到process.nextTick后入微任务队列,依次nextTick1、nextTick3、nextTick2入队后出队输出。之后,在下一个事件循环的timers阶段,执行setTimeout回调输出timeout2。
例3:
setImmediate(function(){
console.log("setImmediate");
setImmediate(function(){
console.log("嵌套setImmediate");
});
process.nextTick(function(){
console.log("nextTick");
})
});
// setImmediate
// nextTick
// 嵌套setImmediate
解释: 事件循环进入check阶段执行回调函数setImmediate,执行同步任务,输出setImmediate,然后check阶段完成。有process.nextTick队列,则输出nextTick。嵌套的setImmediate在下一次事件循环的check阶段执行。
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
对于浏览器:
执行第一个宏任务时,输出同步代码timer1之后,发现微任务then(),执行微任务输出promise1;接着执行第二个宏任务,输出同步任务timer2,发现微任务,执行微任务,输出promise2。
即:timer1 ->promise1->timer2->promise2
浏览器端的处理过程如下:
对于Node情况分两种:
如果是node11版本,与浏览器执行结果一样
如果是node10及以下:要看第一个定时器执行完,第二个定时器是否在完成队列中。
1.全局脚本(main())执行,将2个timer依次放入timer队列,main()执行完毕,调用栈空闲,任务队列开始执行;
2.首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;
3.至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2
node端执行过程:
浏览器和Node 环境下,microtask 任务队列的执行时机不同
参考链接: