了解Event Loop

Event Loop事件循环简介

JavaScript 是单线程的,因为单线程会形成I/O阻塞,好比发送请求时未响应就可能形成页面停滞,为了解决这个问题,浏览器开始支持异步JS,异步JS就是把一些异步任务(ajax、定时器)等放到任务队列中,而后经过事件循环不断读取、触发任务队列中的异步代码,这种机制就叫作事件循环Event Loop。node

Event Loop的核心代码是采用c++写的(属于NodeJs的范畴),本质上来讲Event Loop就是采用轮询的方式不断读取、执行事件,今天咱们要讨论的就是事件循环中的细节。c++

阶段

Event Loop内部分为如下阶段面试

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘复制代码

上面的每个阶段都有一个队列(先进先出),里面存放回调函数。每当Event Loop到达一个阶段,通常来讲都会执行队列中的某些函数(也有可能不操做)ajax

各阶段概览

  • timers 阶段:这个阶段执行 setTimeout 和 setInterval 的回调函数。
  • I/O callbacks 阶段:不在 timers 阶段、close callbacks 阶段和 check 阶段这三个阶段执行的回调,都由此阶段负责,这几乎包含了全部回调函数。
  • idle, prepare 阶段(看起来是两个阶段,不过这不重要):event loop 内部使用的阶段(咱们不用关心这个阶段)
  • poll 阶段:获取新的 I/O 事件。在某些场景下 Node.js 会阻塞在这个阶段。
  • check 阶段:执行 setImmediate() 的回调函数。
  • close callbacks 阶段:执行关闭事件的回调函数,如 socket.on('close', fn) 里的 fn。

一个 Node.js 程序结束时,Node.js 会检查 event loop 是否在等待异步 I/O 操做结束,是否在等待计时器触发,若是没有,就会关掉 event loop。promise

timers

这个阶段颇有多是Event Loop开始的第一个阶段,主要存放setTimeout或者setInterval等宏任务浏览器

poll

这个阶段主要用来获取新的I/O事件,当 event loop 进入 poll 阶段,发现 poll 队列为空,event loop 检查了一下最近的计时器,大概还有 100 毫秒时间,因而 event loop 决定这段时间就停在 poll 阶段,当定时器任务快开始的时候,Event Loop会绕过poll阶段进入check阶段异步

check

这个阶段有一个API,面试的时候常常用的到,属于nodeJS的setImmediate,它一样属于宏任务,可是相对于定时器来讲,它的特色是要求更快执行socket

setImmediate和setTimeout

setImmediate 和 setTimeout 很类似,可是其回调函数的调用时机却不同。async

从所属的阶段队列来看,setImmediate属于check阶段,setTimeout属于timers阶段,那么二者之间到底谁先执行呢?ide

先看一段代码

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

通常来讲,都会优先执行setTmmediate,可是上面的代码实际执行顺序是这样的为何会有时先执行timeout,有时先执行immediate呢,这要从顺序看起,若是上面的代码setTimeout的时间设定为1000ms,那你们必定不会感到困惑,因为immediate存在于check阶段,当时间设定为1000ms时,Event Loop处于poll阶段,毕竟要等到1000ms才执行timers队列中的函数,因此Loop打算休息一下。

而后呢好像时间差很少了,Loop发现check阶段有个immediate函数,因而跑过去执行一下,执行完了就再跑到timers阶段执行。

而上面产生困惑的最大缘由是定时器设置时间为0,这就要看Event Loop开始时,所处的阶段。

若是Event Loop此时在timers阶段,队列中尚未定时器任务,又或者定时器任务还没到时间,那么必然会跳过此阶段,优先执行immediate任务。

若是此时有任务,并且时间到了,那么必然会先执行setTimeout,这也是上述代码产生困惑的缘由。

下面咱们对它进行改写

setTimeout(() => {  setTimeout(() => {console.log("timeout");
  }, 0);

  setImmediate(() => {console.log("immediate");
  });
}, 1000);复制代码

上面的代码,1秒后,执行箭头函数,此时Event Loop并不在timers阶段,因为顺序是不可变的,因此老是会优先执行immediate

process.nextTick()

你可能发现 process.nextTick() 这个重要的异步 API 没有出如今任何一个阶段里,那是由于从技术上来说 process.nextTick() 并非 event loop 的一部分。实际上,无论 event loop 当前处于哪一个阶段,nextTick 队列都是在当前阶段后就被执行了。

setTimeout(() => {  setTimeout(() => {console.log("timeout");
  }, 0);

  setImmediate(() => {console.log("immediate");
  });
  
  process.nextTick(()=>{     console.log('nexTick')
  })
}, 1000);复制代码

上面的代码执行顺序是这样的

nextTick是在当前阶段立刻执行,因为上面的代码执行后Loop处于poll阶段,因此会优先执行nextTick

为了更好得实验,咱们再改一下代码

setTimeout(() => {  setTimeout(() => {console.log("timeout");
    process.nextTick(() => {      console.log("nexTick2");
    });
  }, 0);

  setImmediate(() => {console.log("immediate");
  });

  process.nextTick(() => {console.log("nexTick");
  });
}, 1000);复制代码

下面是结果,能够发现nextTick是在当前阶段立刻执行的

nexTick
immediate
timeout
nexTick2复制代码
process.nextTick() 和 setImmediate()

这两个函数功能很像,并且名字也很使人疑惑。

process.nextTick() 的回调会在当前 event loop 阶段「当即」执行。 setImmediate() 的回调会在后续的 event loop 周期(tick)执行。

两者的名字应该互换才对。process.nextTick() 比 setImmediate() 更 immediate(当即)一些。

这是一个历史遗留问题,并且为了保证向后兼容性,也不太可能获得改善。因此就算这两个名字听起来让人很疑惑,也不会在将来有任何变化。

咱们推荐开发者在任何状况下都使用 setImmediate(),由于它的兼容性更好,并且它更容易理解。

宏任务和微任务

异步任务中分宏任务和微任务,微任务老是比宏任务先执行

常见宏任务

常见微任务

经典面试题

setTimeout(()=> console.log(4))//宏任务new Promise(resolve => {
  resolve()//同步任务
  console.log(1) //同步任务}).then(()=> {  console.log(3) //微任务})console.log(2) //同步任务复制代码

改形成await

setTimeout(_ => console.log(4)) //宏任务async function main() {  console.log(1) //同步任务
  await Promise.resolve() //同步任务 至关于 resolve()
  console.log(3) //至关于promise.then //微任务}
main()console.log(2) //同步任务复制代码
相关文章
相关标签/搜索