AwesomeDevin / blog

Welcome to Devin's blog,I'm trying to be a fullstack developer and sticking with it !!!

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

【Javascript】Event loop 在浏览器端与NodeJS中的差别 以及 NodeJS中关于setTimeout与setImmediate引发的问题

AwesomeDevin opened this issue · comments

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

浏览器端

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

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

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

Nodejs端

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

image

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

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

从技术上来说,poll阶段控制timers什么时候执行,而执行的具体位置在timers
下限的时间有一个范围:[1, 2147483647],如果设定的时间不在这个范围,将被设置为1。

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

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

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

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

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

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

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

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

    • 如果代码没有被设定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')
    })
})

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

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

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

question2

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

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

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

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

Promise.resolve('resolve3').then((data)=>{
    console.log(data)
})

答案

question1

有两种可能性

  • 第一种
timeout1 > timeout2 > resolve1 > resolve2 > setImmediate1 > setImmediate2
  • 第二种
setImmediate1 > setImmediate2 > timeout1 > timeout2 > resolve1 > resolve2 

question2

只有一种可能性

resolve3 > timeout1 > timeout2 > resolve1 > resolve2 > setImmediate1 > setImmediate2