0
点赞
收藏
分享

微信扫一扫

理解Event Loop

Brose 2021-09-30 阅读 80

官方文档: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阶段的两个重要功能:

可以理解为:

  1. 计算出阻塞时间并轮询,直到计时器时间到了,事件循环回到timers阶段,执行计时器的回调函数;
  2. 然后,处理poll队列里面的回调函数。

当event loop进入到poll阶段,而此时又没有计时器,那么:

  1. poll队列不为空时:
    event loop会遍历并同步执行poll队列的回调函数,直到其队列为空或者达到系统上限。
  2. 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

以上代码分析:

  1. 首先执行async1(),输出:1
await async2()
console.log(2)
//等同于
Promise.resolve(async2()).then(()=>{console.log(2)})

所以执行async2()输出:3,并将()=>{console.log(2)}放入微任务

  1. new Promise里面的function马上执行,输出:4
    并将function(){console.log(5)}放入微任务
  2. 目前已输出:1 3 4
    然后看微任务里面,输出:2 5
  3. 所以最后得到:13425
举报

相关推荐

0 条评论