JS 事件循环

此篇文章写的浏览器端的事件循环( EventLoop),Node 端会有所不同。

1 事件循环(EventLoop)

事件循环是 JS 主线程处理任务的过程,包含如下元素:

  • 调用栈(也叫执行栈)
    • 由一系列函数调用组成的栈
  • 消息队列(也叫任务队列、宏任务队列、callback queue)
    • 由待处理的宏任务组成的队列,如 被触发的 click 事件、到达定时时间的任务、已完成的网络请求 等
  • 微队列
    • 由待处理的微任务组成,如 Promise 的回调,MutationObserver 回调等

1.1 事件循环视图

上图展示了事件循环的过程:

  1. 当前正在执行的函数被压入执行栈(stack)中
    1. 函数执行过程中调用 Promise、MutationObserver 等 WebAPI 产生的异步任务需要执行时,会被插入微任务队列(microtask queue) 中。
    2. 函数执行过程中调用 Ajax、setTimeout 等 WebAPI 产生的异步任务需要执行时,会被插入宏任务队列(task queue) 中。
  2. 执行栈中的所有函数执行完成后(即栈空后),JS 主线程会首先从 microtask queue 中取出一个微任务进行执行,执行微任务就是把微任务对应的回调函数压入执行栈中进行执行,即回到步骤 1 。微任务队列为空时才会执行第 3 步。
  3. 微任务队列为空时,JS 主线程再从 task queue 中取出一个宏任务进行执行,执行宏任务也是将宏任务对应的回调函数压入执行栈中进行执行,即回到了步骤 1。

2 堆

对象会被分配到内存中。

3 调用栈(执行栈)

函数被执行时,会产生对应的执行上下文(Execute Context,EC,也叫执行环境),EC 会被压入调用栈 执行。

当有多个函数被嵌套调用时,会有多个 EC 被压入调用栈

1
2
3
4
5
6
7
8
9
10
11
function foo(b) {
let a = 10;
return a + b + 11;
}

function bar(x) {
let y = 3;
return foo(x * y) + y;
}

console.log(bar(7)); // 返回 42

上面代码执行过程:

  1. 主代码的 EC 被压入栈中执行。
  2. 当调用 bar 函数时,bar 的 EC 被压入栈中执行。
  3. 当 bar 调用 foo 时,foo 的 EC 被压入栈中执行。
  4. 当 foo 执行完成时,foo 的 EC 被弹出栈。
  5. 当 bar 执行完成时,bar 的 EC 也被弹出栈。
  6. 主代码块执行完成,其 EC 也被弹出栈,此时栈被清空了。

4 消息队列

消息队列(也叫任务队列宏任务队列callback queuetask queue)由待处理的消息组成,事件循环会依次处理消息队列中的消息。

消息被处理时会被移出消息队列,并作为输入参数来调用与之关联的回调,回调被执行会被压入调用栈中执行。

4.1 宏任务

宏任务(MacroTask,也叫 Task),由下面类型的代码形成,并被加入到宏任务队列:

  • 被触发的事件回调,如 onClick、onLoad

  • setTimeout / setInterval

5 微任务队列

微任务队列由微任务组成,事件循环会依次处理微任务队列中的微任务。

5.1 微任务

微任务(MicroTask,也叫 Jobs),下面类型的代码属于微任务,并被加入微任务队列:

  • Promise

  • MutationObserver

    • 监控 DOM 树的更改

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      // 选择需要观察变动的节点
      const targetNode = document.getElementById('some-id');

      // 观察器的配置(需要观察什么变动)
      const config = { attributes: true, childList: true, subtree: true };

      // 当观察到变动时执行的回调函数
      const callback = function(mutationsList, observer) {
      // Use traditional 'for loops' for IE 11
      for(let mutation of mutationsList) {
      if (mutation.type === 'childList') {
      console.log('A child node has been added or removed.');
      }
      else if (mutation.type === 'attributes') {
      console.log('The ' + mutation.attributeName + ' attribute was modified.');
      }
      }
      };

      // 创建一个观察器实例并传入回调函数
      const observer = new MutationObserver(callback);

      // 以上述配置开始观察目标节点
      observer.observe(targetNode, config);

      // 之后,可停止观察
      observer.disconnect();
  • queueMicrotask

    • ```js
      queueMicrotask(() => {
      /* 微任务中将运行的代码 */
      });
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71

      - 使用场景

      - 代码执行结果的一致性

      - 串行执行任务



      # 6 执行顺序

      JS 主线程执行顺序:

      1. 当前执行栈中的代码
      2. 当前执行栈所产生的 microtask(直到微任务队列中任务全部执行完成,即为空时才进行下一步)
      3. 渲染绘制
      4. 下个 task



      示例:

      ```js
      setTimeout(function() {
      console.log(5)
      setTimeout(function(){
      console.log(9);
      })

      Promise.resolve().then(function() {
      console.log(6);
      })
      }, 0);

      new Promise(function executor(resolve) {
      console.log(1);

      setTimeout(function(){
      console.log(7);
      });

      Promise.resolve().then(function() {
      console.log(3);

      Promise.resolve().then(function() {
      console.log(4);

      setTimeout(function(){
      console.log(8);
      });
      })
      })

      }).then(function() {
      console.log(4);
      });


      console.log(2);

      //输出顺序为:
      1
      2
      3
      4
      undefined
      5
      6
      7
      8
      9

参考资料:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop

https://www.ruanyifeng.com/blog/2014/10/event-loop.html