官方文档:The Node.js Event Loop, Timers, and process.nextTick()
什么是Event Loop
在官方文档里这样写到:
也就是说,尽管JavaScript是单线程的,有了Event Loop,才允许Node.js去执行非阻塞的I/O操作,尽可能的把这些操作转移给系统内核。
由于大多数现代内核都是多线程的,它们可以处理在后台执行的多个操作。当某个操作完成了,内核会通知Node.js,以便将对应的回调函数添加到轮询队列(poll),最终执行。
Event Loop 具体解释
I: 当Node.js启动时;
II: 会初始化Event Loop;
III: 并执行脚本,这些脚本可能会调用异步API、计时器,或者调用process.nextTick()
;
IV: 接着就开始处理Event Loop。
Event Loop直译就是事件循环,在循环中经历了如下几个阶段:
阶段概述
- timers:执行
setTimeout()
、setInterval()
的回调函数; - pending callbacks:执行I/O回调,执行不在timers、check、close callbacks执行的所有回调;
- idle, prepare:此阶段仅内部使用;
- poll:获取新的I/O事件,执行I/O相关回调,某些情况node会阻塞在这里;
- check:
setImmediate()
的回调函数会在此阶段执行; - close callbacks:一些close事件的回调函数会在此阶段执行,例:
socket.on('close', ...)
。
example:
...
setTimeout(()=>console.log('fn'), 1000)
...
启动node,初始化Event Loop;
当脚本运行到setTimeout时;
就开始处理Event Loop了;
setTimeout的回调函数放在timers阶段;
当poll队列为空时,event loop会检测计时器,到了1000毫秒时,会经过check阶段回到timers阶段执行计时器的回调函数,打印出:fn。
poll阶段
poll是比较重要的一个阶段,因为上下衔接了Event Loop的各个阶段。
原文说明了poll阶段的两个重要功能:
可以理解为:
- 计算出阻塞时间并轮询,直到计时器时间到了,事件循环回到timers阶段,执行计时器的回调函数;
- 然后,处理poll队列里面的回调函数。
当event loop进入到poll阶段,而此时又没有计时器,那么:
- poll队列不为空时:
event loop会遍历并同步执行poll队列的回调函数,直到其队列为空或者达到系统上限。 - poll队列为空时:
I: 若脚本设置了setImmediate()
的回调,event loop会结束poll阶段,进入check阶段执行setImmediate()
的回调函数
II: 若没有setImmediate()
,event loop会等待回调函数加入poll队列,并马上执行掉。
当poll队列为空时,event loop会检测是否有已经到期的计时器,若存在则按照循环顺序绕回timers阶段执行计时器的回调函数。
那么,当setTimeout和setImmediate同时存在时,会是什么样的执行顺序呢?
example1:
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'), 0);
以上代码答案是不确定的,多次测试发现,一会是先'setTimeout'后'setImmediate',一会是先'setImmediate'后'setTimeout',因为整个事件循环中没有办法确定是在哪个阶段开始的。
example2:
setTimeout(()=>{
setImmediate(() => console.log('setImmediate'), 0);
setTimeout(() => console.log('setTimeout'), 0);
}, 1000)
以上代码答案一定是:setImmediate setTimeout
因为整个事件循环开启后,确定是先在poll阶段等待停留之后,进入check阶段,而check一定会执行setImmediate回调,再绕回timers阶段执行setTimeout回调。
process.nextTick()
尽管process.nextTick()也属于异步API,但它不存在于事件循环中的任何阶段。
nextTick队列一定是在当前操作完成后紧接着处理,无论是在事件循环的哪个阶段。
example:
setTimeout(()=>{
setTimeout(()=>console.log('fn1'), 0)
setImmediate(()=>console.log('fn2'))
process.nextTick(()=>console.log('fn3'))
}, 1000)
结果: fn3 fn2 fn1
分析:整个事件循环开启后,确定是先在poll等待停留之后,在进入check阶段之前执行nextTick回调(fn3),下一步是check阶段执行setImmediate回调(fn2),再绕回timers阶段执行setTimeout回调(fn1)。
宏任务 微任务
Eventloop在Chrome有两个阶段:
宏任务:MacroTask
微任务:MicroTask
当宏任务和微任务同时出现时,一定是先执行微任务再执行宏任务;
当宏任务里面包含微任务时,先执行微任务。
Chrome里面的宏任务微任务:
宏任务:setTimeout
微任务:promise.then(fn)、await也是转化为promise处理
exmaple:
async function async1(){
console.log(1)
await async2()
console.log(2)
}
async function async2(){
console.log(3)
}
async1()
new Promise(function(resolve){
console.log(4)
resolve()
}).then(function(){
console.log(5)
})
//13425
以上代码分析:
- 首先执行async1(),输出:1
await async2()
console.log(2)
//等同于
Promise.resolve(async2()).then(()=>{console.log(2)})
所以执行async2()输出:3,并将()=>{console.log(2)}
放入微任务
- new Promise里面的function马上执行,输出:4
并将function(){console.log(5)}
放入微任务 - 目前已输出:1 3 4
然后看微任务里面,输出:2 5 - 所以最后得到:13425