JavaScript为什么要有微任务?

2,622 阅读5分钟

背景

会研究这个问题源自于群里一个问题,下面是原问题:

为什么要有微任务呢,只用宏任务不行吗?假设让你设计事件循环,你会如何设计?是不是必须这样,不这样会有什么问题?假如脱离浏览器,有没有其它场景?

一共 4 问,我们一步一步来。

为什么要有微任务,只用宏任务不行吗?

要回答这个问题,首先要了解一下 Web 的发展历程。

早期的 JavaScript

在一开始,浏览器是 JS 负责处理用户交互以及逻辑的,处理完毕就开始渲染。这个时候的执行逻辑像是下面这样:

网页加载,执行 JS 脚本->浏览器渲染->等待用户交互->处理交互相关的 JS 脚本->渲染...

但是这样会面临 JS 执行时间过长,页面长时间无响应,用户体验差

任务的出现

为了解决这个问题,提供了异步的机制,也就是利用 setTimeoutsetinterval 创建异步任务。这个时候就变成了这样:

网页加载,执行 JS 脚本(无异步任务)->浏览器渲染->等待用户交互->处理交互相关的 JS 脚本->渲染...


网页加载,执行 JS 脚本(有异步任务)->浏览器渲染->处理异步任务->等待用户交互->处理交互相关的 JS 脚本->渲染...

无论是首次加载网页,还是用户交互执行的 JS 脚本,或者是 setTimeout 等创建的异步任务,都是任务,也就是“宏任务”。所以总结起来就是:

宏任务->渲染->宏任务->渲染...

以上参考自 MDN:

自从 (setTimeout() 和 setInterval()) 加入到 Web API 后,浏览器提供的 JavaScript 环境就已经逐渐开始包含了任务安排、多线程应用开发等强大的特性。了解 JavaScript 运行时是如何安排和运行代码的对了解 queueMicrotask() 会非常有作用。——MDN

每个代理都是由事件循环驱动的,事件循环负责收集用事件(包括用户事件以及其他非用户事件等)、对任务进行排队以便在合适的时候执行回调。然后它执行所有处于等待中的 JavaScript 任务(宏任务),然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操作。——MDN

为什么要有微任务

微任务的出现,使得在宏任务执行完,到浏览器渲染之前,可以在这个阶段插入任务的能力

上几个引用供参考:

一个 微任务(microtask)就是一个简短的函数,当创建该函数的函数执行之后,并且 只有当 Javascript 调用栈为空,而控制权尚未返还给被 user agent 用来驱动脚本执行环境的事件循环之前,该微任务才会被执行。 事件循环既可能是浏览器的主事件循环也可能是被一个 web worker 所驱动的事件循环。这使得给定的函数在没有其他脚本执行干扰的情况下运行,也保证了微任务能在用户代理有机会对该微任务带来的行为做出反应之前运行。 ——MDN

通过使用异步 JavaScript技术(例如承诺)允许主代码在等待请求结果的同时继续运行,进一步缓解了这种情况。但是,在更基础级别上运行的代码(例如包含库或框架的代码)可能需要一种方法来安排代码在安全时间运行,同时仍在主线程上执行,而与任何单个请求的结果或任务。——MDN

以及「What was the motivation for introducing a separate microtask queue which the event loop prioritises over the task queue?」这个问题下的回答。

只用宏任务不行吗?

先看下面一段代码:

<html>
<head>
  <style>
    div {
      padding: 0;
      margin: 0;
      display: inline-block;
      widtH: 100px;
      height: 100px;
      background: blue;
    }
    #microTaskDom {
      margin-left: 10px;
    }
  </style>
</head>
<body>
  <div id="taskDom"></div>
  <div id="microTaskDom"></div>
  <script>
    window.onload = () => {
        setTimeout(() => {
          taskDom.style.background = 'red'
          setTimeout(() => {
            taskDom.style.background = 'black'
          }, 0);
          
          microTaskDom.style.background = 'red'
          Promise.resolve().then(() => {
            microTaskDom.style.background = 'black'
          })
        }, 1000);
    }
  </script>
</body>

</html>

如果使用 setTimeout 立马修改背景色,会闪现一次红色背景,而使用 Promise 则不会。

因为微任务会在渲染之前完成对背景色的修改,等到渲染时就只需要渲染黑色。

而使用任务,第一次渲染会渲染红色,等到下一次任务时修改为黑色,之后的渲染阶段才会再次渲染为黑色。

大家可以拷贝上面的代码自行尝试一下,看下效果。

h19y5-cadju.gif

假设让你设计事件循环,你会如何设计?

目前这种设计基本可以了,在渲染前以及渲染后都已经可以完成一系列操作。

是不是必须这样,不这样会有什么问题?

不是,虽然我没遇到过需要利用这个机制处理的场景,但是基本上都能有解决办法。

宏任务以及微任务的出现,都是从用户体验以及性能方面进行考虑的,它们的出现可以让用户得到更好的使用体验。

假如脱离浏览器,有没有其它场景?

宏任务和微任务的出现,实际上是让 JS 脚本有了在渲染阶段前后可以完成一些操作的能力,类似于生命周期的概念。

所以像Vue、React的生命周期,Node.js 的事件循环都是一种场景。

以下是 JS 版本的一段伪码:

总结

技术的发展是不断精进的,JS 也不是一下子就到了现在这么完善的地步,都是一步一步根据问题来的。

而宏任务和微任务,本质上也是让 JS 脚本能够在渲染阶段前后,有了完成某些功能的能力。


为了帮助更多人拥有像我一样的学习能力,我制作了《极简化学习》这个系列,欢迎了解。