Event loop 在浏览器端与NodeJS中的差异 以及关于setTimeout与setImmediate引起的问题

组内每周都会有分享总结会,昨晚分享的课题是Event Loop,我很积极且有点自信地答了几道题,结果被虐的体无完肤,果真有些东西不常常回顾就容易忘,因而花一夜挑灯夜战从新作了一份有关event loop的知识总结,在此分享给你们,但愿对各位看官有所帮助,看完有收获的同窗还请积极点赞,讲的不对的地方,望指出,我会及时修正,谢谢~javascript

浏览器端

浏览器端的event loop基于javascript中的堆/栈/任务队列,任务队列又分为宏任务微任务java

每次事件循环的时候:
  • 微任务/宏任务在相同做用域下,会先执行微任务,再执行宏任务
  • 宏任务处于微任务做用域下,会先执行微任务,再执行微任务中的宏任务
  • 微任务处于宏任务做用域下时,会先执行宏任务队列中的任务,而后再执行微任务队列中的任 务,在当前的微任务队列没有执行完成时,是不会执行下一个宏任务的。

本文主要讲解的仍是Node,对于浏览器端event loop的具体分析及证实能够查看这篇文章探究javascript中的堆/栈/任务队列与并发模型 event loop的关系node

Nodejs端

nodejs 的事件循环分为6个阶段,每一个阶段都有1个任务队列,微任务在事件循环的各个阶段之间执行

image

timers 阶段: 这个阶段执行timer(setTimeout、setInterval)的回调

一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定的时间事后,timers会尽早的执行回调,可是系统调度或者其余回调的执行可能会延迟它们。git

从技术上来讲,poll阶段控制timers何时执行,而执行的具体位置在timers。 下限的时间有一个范围:[1, 2147483647],若是设定的时间不在这个范围,将被设置为1。github

I/O callbacks 阶段:执行大多回调以及一些系统调用错误,好比网络通讯的错误回调

idle, prepare 阶段: 仅node内部使用

poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里

poll阶段有两个主要的功能:
1. 是执行下限时间已经达到的timers的回调
2. 是处理poll队列里的事件。promise

注:Node不少API都是基于事件订阅完成的,这些API的回调应该都在poll阶段完成。 当事件循环进入poll阶段:浏览器

  • poll队列不为空的时候,事件循环确定是先遍历队列并同步执行回调,直到队列清空或执行回调数达到系统上限。网络

  • poll队列为空的时候,这里有两种状况。并发

    • 若是代码已经被setImmediate()设定了回调,那么事件循环直接结束poll阶段进入check阶段来执行check队列里的回调。socket

    • 若是代码没有被设定setImmediate()设定回调:

      • 若是有被设定的timers,那么此时事件循环会检查timers,若是有一个或多个timers下限时间已经到达,那么事件循环将绕回timers阶段,并执行timers的有效回调队列。
      • 若是没有被设定timers,这个时候事件循环是阻塞在poll阶段等待回调被加入poll队列。

check 阶段:执行 setImmediate() 的回调

这个阶段容许在poll阶段结束后当即执行回调。若是poll阶段空闲,而且有被setImmediate()设定的回调,那么事件循环直接跳到check执行而不是阻塞在poll阶段等待回调被加入。

注:事件循环运行到check阶段的时候,setImmediate()具备最高优先级,只要poll队列为空,代码被setImmediate(),不管是否有timers达到下限时间,setImmediate()的代码都先执行

close callbacks 阶段:执行 socket 的 close 事件回调

若是一个sockethandle被忽然关掉(好比socket.destroy()),close事件将在这个阶段被触发,不然将经过process.nextTick()触发。

NodeJS中关于setTimeout与setImmediate引起的问题

问题引入

setTimeout(()=>{
    console.log('timer')
})
setImmediate(()=>{
    console.log('immediate')
})
复制代码

能够发现结果存在随机性

缘由

首先进入的是timers阶段,若是咱们的机器性能通常,那么进入timers阶段,一毫秒已通过去了,那么setTimeout的回调会首先执行。

若是没有到一毫秒,那么在timers阶段的时候,下限时间没到,setTimeout回调不执行,事件循环来到了poll阶段,这个时候队列为空,此时有代码被setImmediate(),因此进入check阶段,先执行了setImmediate()的回调函数,以后在下一个事件循环再执行setTimemout的回调函数。

而咱们在执行代码的时候,进入timers的时间延迟实际上是随机的,并非肯定的,因此会出现两个函数执行顺序随机的状况。

咱们再来看一段代码

fs.readFile('./main.js',()=>{
    setTimeout(()=>{
        console.log('timer')
    })
    setImmediate(()=>{
        console.log('immediate')
    })
})
复制代码

能够发现 setImmediate永远先于 setTimeout执行

缘由

fs.readFile的回调是在poll阶段执行的,回调执行完毕后poll阶段的队列为空,因而进入check阶段,执行setImmediate回调,而setTimeout的回调须要等到下一个事件循环的timers阶段才去执行

NodeJS中的process.nextTick() and Promise

对于这两个,咱们能够把它们理解成一个微任务。也就是说,它其实不属于事件循环的一部分。

那么他们是在何时执行呢?

无论在什么地方调用,他们都会在其所处的事件循环最后,在事件循环进入下一个循环的阶段前执行,可是nextTick优先于promise执行。 process.nextTick()会在各个事件阶段之间执行,一旦执行,要直到nextTick队列被清空,才会进入到下一个事件阶段,因此若是递归调用 process.nextTick()/promise,会致使出现I/O starving(饥饿)的问题,推荐使用setImmediate()

看了这么多,如今你们作两道题吧,检测本身是否真的理解了

question1

setTimeout(() => {
  console.log('timeout1')
  Promise.resolve().then(()=>{
        console.log('reslove1')
    })
}, 0)

setTimeout(() => {
  console.log('timeout2')
  Promise.resolve().then(()=>{
        console.log('reslove2')
    })
}, 0)

setImmediate(()=>{
    console.log('setImmediate1')
})

setImmediate(()=>{
    console.log('setImmediate2')
})

复制代码

question2

setTimeout(() => {
  console.log('timeout1')
  Promise.resolve().then(()=>{
        console.log('reslove1')
    })
}, 0)

setTimeout(() => {
  console.log('timeout2')
  Promise.resolve().then(()=>{
        console.log('reslove2')
    })
}, 0)

setImmediate(()=>{
    console.log('setImmediate1')
})

setImmediate(()=>{
    console.log('setImmediate2')
})

Promise.resolve('resolve3').then((data)=>{
    console.log(data)
})
复制代码

篇幅很长,很是感受你看完了个人文章,谢谢~,答案会公布在issue