【事件循环Event Loop取消了宏任务?】

前序

众所周知,JavaScript是一门单线程语言,它运行在浏览器的渲染主线程中,而渲染主线程只有一个。但是渲染主线程承担着诸多工作如:渲染页面等。

如果所有代码都是同步的话,如果一个代码卡住了,就会导致主线程代码的阻塞,从而导致运行性能效率受到极大的影响。
所以浏览器采用了异步的方式,来避免这种情况的发生。具体是当某些任务发生时,如计时器、网络、事件监听等,主线程将任务交给其他线程去计时或监听,当主线程无事可做时或计时监听触发时,将任务加入到消息队列的末尾排队,等待主线程调度。

宏任务与微任务

在过去的JavaScript中,异步任务又分为宏任务和微任务。
JavaScript 引擎遵循事件循环的机制,在执行完当前宏任务后,会检查微任务队列,执行其中的微任务,然后再取下一个宏任务执行。这个过程不断循环,形成事件循环。
在这里插入图片描述

1、宏任务(Macrotasks)是一些较大粒度的任务,包括:

所有同步任务
I/O操作,如文件读写、数据库数据读写等
setTimeout、setInterval
setImmediate(Node.js环境)
requestAnimationFrame
事件监听回调函数等

2、微任务(Microtasks)是一些较小粒度、高优先级的任务,包括:

Promise的then、catch、finally
async/await中的代码
Generator函数
MutationObserver
process.nextTick(Node.js 环境)

事件循环(Event Loop)

事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
在Chrome中,它开启一个不会结束的for循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入队列末尾。
过去把消息队列简单分为宏任务微任务,但是随着浏览器复杂度急剧提升,W3C已经不再使用宏任务的说法。

根据W3C的最新解释:
每个任务都有一个任务类型,同一个类型的任务必须在一个队列里,不同类型的任务分属于不同的队列,在一次事件循环中,浏览器可以根据实际情况从不同队列中取出任务执行。大概有以下几个的队列DOM操作、用户交互、网络请求、网页导航和历史记录遍历、渲染 查看原文
在这里插入图片描述

同时浏览器必须准备好一个微队列,微队列中的任务优先于所有其他任务执行。查看原文
在这里插入图片描述

在目前的Chrome实现中,至少包含了以下队列:

在这里插入图片描述

此处的优先级仅方便于我们对于谷歌浏览器队列的实现的方便展现,并未在W3C中强约束优先级,所以每个浏览器的实现方式可能些许不同。Chrome中队列的优先级可以查看task_type.h文件

演示

那有人就会问了,如果是按照这个说法,很多关于消息队列的面试题岂不是会混乱,这么大的事情应该大家都知道吧,在实际开发中,有什么区别呢,我们上代码试试。

首先,我们需要一个阻塞主线程的函数

 // delay函数 传入一个时间,使用循环,阻塞主线程的运行,方便我们后续的操作
 function delay(duration) {
  const now = Date.now();
  while (Date.now() - now < duration) { }
}

网络队列,在网络队列中,我们通过调用fetch然后加上delay函数,让函数进入队列。

function addNetWork() {
  fetch('./1.json').then((res) => {
    console.log('网络队列执行');
  })
  console.log('添加网络队列');
  delay(2000)
}

延时队列,我们使用setTimeout函数加上delay,延时为0,让函数进入队列。

 function addDelay() {
  setTimeout(() => {
    console.log('延时队列执行');
  }, 0)
  console.log('添加延时队列');
  delay(2000)
}

交互队列,我们给一个按钮绑定onclick事件,然后再绑定事件后,点击按钮,让函数进入队列。

function addInteraction() {
  interaction.onclick = function () {
    console.log('交互队列执行');
  }
  console.log('添加交互队列');
  delay(2000)
}

为了方便,我们需要一个主函数去触发上面三个方法。

begin.onclick = function () {
  addDelay()
  addNetWork()
  addInteraction()
  console.log('添加成功');
}

因为node和浏览器实现事件循环的方式不同,所以我们使用Chrome浏览器测试
完整代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<button id="begin">开始</button>
<button id="interaction">添加交互任务</button>

<body>
  <script>
    const begin = document.querySelector("#begin");
    const interaction = document.querySelector("#interaction");
    // delay函数 传入一个时间,使用循环,阻塞主线程的运行,方便我们后续的操作
    function delay(duration) {
      const now = Date.now();
      while (Date.now() - now < duration) { }
    }

    function addDelay() {
      setTimeout(() => {
        console.log('延时队列执行');
      }, 0)
      console.log('添加延时队列');
      delay(2000)
    }

    function addNetWork() {
      fetch('./1.json').then((res) => {
        console.log('网络队列执行');
      })
      console.log('添加网络队列');
      delay(2000)
    }

    function addInteraction() {
      interaction.onclick = function () {
        console.log('交互队列执行');
      }
      console.log('添加交互队列');
      delay(2000)
    }

    begin.onclick = function () {
      addDelay()
      addNetWork()
      addInteraction()
      console.log('添加成功');
    }

  </script>
</body>
</html>

分析

此时,我们使用宏任务与微任务的方式对我们的队列进行分析
在这里插入图片描述

预期的打印结果是

添加延时队列
添加网络队列
添加交互队列
添加成功
延时队列执行
网络队列执行
交互队列执行

但是实际运行结果如下(注意:在控制台打印添加交互队列时,点击了添加交互任务按钮)
在这里插入图片描述

我们发现与我们之前预期的宏任务微任务结果不一致,交互队列执行代码早于延时与网络队列。所以可以证明,浏览器对于不同类型的队列,有不同的处理时机。这里预测是因为浏览器希望用户点击后,可以尽快响应,而不用等待延时或者网络等队列的执行。所以也可以证明谷歌浏览器实现已经不是简单的宏任务与微任务了。
新的分析图应该为:
在这里插入图片描述

拓展

那我们还可以提出问题,那网络队列和延时队列之间呢?
我们交换一下启动函数的代码顺序:

 begin.onclick = function () {
      addNetWork()
      addDelay()
      addInteraction()
      console.log('添加成功');
    }

此时控制台变为:

添加网络队列
添加延时队列
添加交互队列
添加成功
交互队列执行
网络队列执行
延时队列执行

我们可以看到,网络和延时队列的执行还是按照宏任务的方式,谁先进入队列,谁先运行。他们的优先级是相同的。

那微队列有没有变化呢?

我们修改一下代码,在我们运行队列时添加一个微队列,完整代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<button id="begin">开始</button>
<button id="interaction">添加交互任务</button>

<body>
  <script>
    const begin = document.querySelector("#begin");
    const interaction = document.querySelector("#interaction");
    // delay函数 传入一个时间,使用循环,阻塞主线程的运行,方便我们后续的操作
    function delay(duration) {
      const now = Date.now();
      while (Date.now() - now < duration) { }
    }
    // 创建一个微队列任务
    function microTask() {
      Promise.resolve().then(() => {
        console.log('微队列执行');
      })
    }

    function addDelay() {
      setTimeout(() => {
        console.log('延时队列执行');
        microTask()
      }, 0)
      console.log('添加延时队列');
      delay(2000)
    }

    function addNetWork() {
      fetch('./1.json').then((res) => {
        console.log('网络队列执行');
        microTask()
      })
      console.log('添加网络队列');
      delay(2000)
    }

    function addInteraction() {
      interaction.onclick = function () {
        console.log('交互队列执行');
        microTask()
      }
      console.log('添加交互队列');
      delay(2000)
    }

    begin.onclick = function () {
      addNetWork()
      addDelay()
      addInteraction()
      console.log('添加成功');
    }

  </script>
</body>
</html>

执行结果是:

添加网络队列
添加延时队列
添加交互队列
添加成功
交互队列执行
微队列执行
网络队列执行
微队列执行
延时队列执行
微队列执行

我们可以看到对于微队列的执行是没有变化的,当微队列有事件时,会先执行微队列中的事件。

结论

在W3C中对于事件循环(Event Loop)的处理已经不再是之前的宏任务与微任务的方式了,每个任务都有一个任务类型,同一个类型的任务必须在一个队列里,不同类型的任务分属于不同的队列,在一次事件循环中,浏览器可以根据实际情况从不同队列中取出任务执行。但是浏览器也必须准备好一个微队列(microtask),微队列中的任务优先于所有其他任务执行。总之就是一句话W3C将原来的宏任务更加细粒度,将其拆分为了多个队列,每个队列根据浏览器差异设置优先级。比如在Chrome中有网络队列、延时队列、交互队列 而优先级为:微任务 > 宏任务(交互队列 > 网络队列 = 延时队列)

本文转载至掘金文章:JS事件循环(Event Loop)改了?宏任务不存在了?让我们来看看最新的是什么样的

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值