1. 什么是eventLoop?

它是一个在 JavaScript 引擎等待任务,执行任务和进入休眠状态等待更多任务这几个状态之间转换的无限循环。 我们都知道JavaScript引擎是单线程的,至于为什么是单线程主要是出于JavaScript的使用场景考虑,作为浏览器的脚本语言,js的主要任务是主要是实现用户与浏览器的交互,以及操作dom,如果设计成多线程会增加复杂的同步问题。想象一个场景:多个线程同时操作dom,浏览器渲染引擎该使用哪个线程的结果。当然为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

虽然JS是单线程的,但浏览器却是多线程,其中几个典型的线程已经在图中表示出来,而 eventLoop就是沟通JS引擎线程和浏览器线程的桥梁,也是浏览器实现异步非阻塞模型的关键

2. 宏队列和微队列:

宏队列,macrotask,也叫tasks。 一些异步任务的回调会依次进入macro task queue,等待后续被调用,这些异步任务包括:

  • setTimeout
  • setInterval
  • setImmediate (Node独有)
  • requestAnimationFrame (浏览器独有)
  • UI rendering (浏览器独有)
  • 微队列,microtask,也叫jobs。 另一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:

  • process.nextTick (Node独有)
  • Promise
  • Object.observe
  • MutationObserver
  • 3. 浏览器事件循环流程简图

    3.1 浏览器EventLoop的具体流程:

  • js引擎将所有代码放入执行栈,并依次弹出并执行,这些任务有的是同步有的是异步(宏任务或微任务)。
  • 如果在执行 栈中代码时发现宏任务则交个浏览器相应的线程去处理,浏览器线程在正确的时机(比如定时器最短延迟时间)将宏任务的消息(或称之为回调函数)推入宏任务队列。而宏任务队列中的任务只有执行栈为空时才会执行。
  • 如果执行 栈中的代码时发现微任务则推入微任务队列,和宏任务队列一样,微任务队列的任务也在执行栈为空时才会执行,但是微任务始终比宏任务先执行。
  • 当执行栈为空时,eventLoop转到微任务队列处,依次弹出首个任务放入执行栈并执行,如果在执行的过程中又有微任务产生则推入队列末尾,这样循环直到微任务队列为空。
  • 当执行栈和微任务队列都为空时,eventLoop转到宏任务队列,并取出队首的任务放入执行栈执行。需要注意的是宏任务每次循环只执行一个。
  • 重复1-5过程
  • ...直到栈和队列都为空时,代码执行结束。引擎休眠等待直至下次任务出现。
  • 3.2 在这个过程中有三个重点:

    1. 宏任务每次只取一个,执行之后马上执行微任务。

    2. 微任务会依次执行,直到微任务队列为空。

    3. 图中没有画UI rendering的节点,因为这个是由浏览器自行判断决定的,但是只要执行UI rendering,它的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render。

    看到这里相信任何事件循环判断的题都难不倒你了,做个小测试检验一下吧:

    console.log(1);
    setTimeout(() => {
      console.log(2);
      Promise.resolve().then(() => {
        console.log(3)
    new Promise((resolve, reject) => {
      console.log(4)
      resolve(5)
    }).then((data) => {
      console.log(data);
      Promise.resolve().then(() => {
        console.log(6)
      }).then(() => {
        console.log(7)
        setTimeout(() => {
          console.log(8)
        }, 0);
    
    setTimeout(() => {
       console.log(9)
    }, 0);
    console.log(10)

    答案:1,4,10,5,6,7,2,3,9,8  你做对了吗? 如果还不明白的话对照图在走一次。

    4. Node.JS事件循环流程简图

    可以看出Node.JS的事件循环比浏览器端复杂很多。

    5. NodeJS中的宏队列和微队列

    5.1 事实上NodeJS中执行宏队列的回调任务有6个阶段,按如下方式依次执行:

  • timers阶段:这个阶段执行setTimeout和setInterval预定的callback。
  • I/O callback阶段:执行除了close事件的callbacks、被timers设定的callbacks、setImmediate()设定的callbacks这些之外的callbacks。
  • idle, prepare阶段:仅node内部使用。
  • poll阶段:获取新的I/O事件,适当的条件下node将阻塞在这里。
  • check阶段:执行setImmediate()设定的callbacks。
  • close callbacks阶段:执行socket.on('close', ....)这些callbacks。
  • 5.2 其中宏队列有4个,各种类型的任务主要集中在以下四个队列之中

  • Timers Queue
  • IO Callbacks Queue
  • Check Queue
  • Close Callbacks Queue
  •    微队列主要有2个,不同的微任务放在不同的微队列中:

  • Next Tick Queue:是放置process.nextTick(callback)的回调任务的
  • Other Micro Queue:放置其他microtask,比如Promise等
  • 6. Node的 EventLoop的具体流程:

  • 执行全局Script的同步代码。
  • 执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务。
  • 执行macrotask宏任务,共6个阶段,从第1个阶段开始执行相应每一个阶段macrotask中的所有任务,注意,这里是所有每个阶段宏任务队列的所有任务,在浏览器的Event Loop中是只取宏队列的第一个任务出来执行,每一个阶段的macrotask任务执行完毕后,开始执行微任务,也就是步骤2。
  • Timers Queue -> 步骤2 -> I/O Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue ......
  • 重复1 - 4过程。
  • 面做一个小测试吧

    console.log(0);
    setTimeout(() => {          // callback1
      console.log(1);
      setTimeout(() => {        // callback2
        console.log(2);
      }, 0);
      setImmediate(() => {      // callback3
        console.log(3);
      process.nextTick(() => {  // callback4
        console.log(4);  
    }, 0);
    setImmediate(() => {        // callback5
      console.log(5);
      process.nextTick(() => {  // callback6
        console.log(6);  
    setTimeout(() => {          // callback7              
      console.log(7);
      process.nextTick(() => {  // callback8
        console.log(8);   
    }, 0);
    process.nextTick(() => {    // callback9
      console.log(9);  
    console.log(10);

    答案:0, 10, 9, 1, 4, 7, 8, 5, 6, 3, 2 

    7. 总结

    1. 事件循环是 浏览器 和 Node 执行JS代码的核心机制,但浏览器 和 NodeJS事件循环的实现机制有些不同。

    2. 浏览器事件循环有一个宏队列,一个微队列,且微队列在执行过程中一个接一个执行一直到队列为空,宏队列只取队首的一个任务放入执行栈执行,执行过后接着执行微队列,并构成循环。

    3. NodeJS事件循环有四个宏队列,两个微队列,微队列执行方式和浏览器的类似,先执行Next Tick Queue所有任务,再执行Other Microtask Queue所有任务。 但宏队列执行时会依次执行队列中的每个任务直至队为空才开始再次执行微队列任务。

    4. MacroTask包括: setTimeout、setInterval、 setImmediate(Node)、requestAnimation(浏览器)、IO、UI rendering

    5. Microtask包括: process.nextTick(Node)、Promise、Object.observe、MutationObserver

    8. 后记

    学习事件循环会让我们对JS引擎执行代码流程有一个大概的了解,如果遇到任务执行顺序带来的问题,我们也能更快的解决。 同时也会让我们对异步编程有一个更深的认识。