JavaScript的宏任务与微任务

11,991 阅读6分钟

在介绍前端宏任务与微任务之前,先列出来一道题,一块看一下。

console.log('1')
setTimeout(() => {
  console.log('2')
})
new Promise((resolve, rejects) => {
  console.log('3')
  resolve()
}).then(() => {
  console.log('4')
})
console.log(5)

诸位可以先给出来一个自己的答案,运行一下结果,看看是否与自己想的一致。

1.基本概念

这里介绍一下JavaScript里面的一些基本知识

  1. 关于代码执行环境,JavaScript代码执行时,引擎会创造出来当前代码块的执行环境,在涉及到使用变量时,只能查找到当前环境的变量和包含当前执行环境的外部环境变量。全局环境是最外层的执行环境
  2. JavaScript是单线程
  3. JavaScript在处理异步操作时,利用的是事件循环机制。

2.宏任务、微任务与事件循环机制

了解事件循环的同学都知道,在事件循环中,异步事件并不会放到当前任务执行队列,而是会被挂起,放入另外一个回调队列。当前的任务队列执行结束以后,JavaScript引擎回去检查回调队列中是否有等待执行的任务,若有会把第一个任务加入执行队列,然后不断的重复这个过程。

从现象上来看,宏任务和微任务产生的异步操作,都会在执行队列完成后再执行,所以貌似宏任务和微任务都放到回调队列中。

真的是这样吗?

肯定不是。如果真的是这样,那宏任务和微任务在意义上便没有区别了。

3.宏任务与微任务

首先我们肯定要坚持一点:宏任务和微任务在意义上肯定是有绝对区别的。

看一下在浏览器环境下能够触发宏任务的操作都有哪些(其他环境下会有不同):

  1. I/O 操作
  2. setTimeout
  3. setInterval
  4. requestAnimationFrame(争议,后面会讨论)

以 setTimeout 为例。由于 JavaScript 是单线程,所以 setTimeout 的计时操作一定不是JavaScript来做的,否则会造成代码执行的阻塞。

那么这种操作是由谁来做的?是宿主环境。以浏览器为例子,JavaScript 在执行到 setTimeout 时会告诉浏览器:“Hey boy!这有个定时器,你帮我看着点,等到点了你告诉我一下”。这时候浏览器就会进行一个计时操作,计时完成以后,将 setTimeout 的回调放入 JavaScript 事件循环的回调队列中。这样 JavaScript 就可以在接下来的执行中处理这个回调。

我们看一下上面列出来的4点触发宏任务的操作,全部与浏览器相关!

所以,我个人的理解是:宏任务便是 JavaScript 与宿主环境产生的回调,需要宿主环境配合处理并且会被放入回调队列的任务都是宏任务。

浏览器下触发微任务的操作为:

  1. Promise
  2. MutationObserver

这两个操作也都能够产生异步操作,那为什么与宏任务不一样呢。这里就要涉及到事件循环的另一个队列了--作业队列(微任务队列)。

为了更好的理解作业队列,我们把执行队列从开始到结束这样的一个过程,称为一个tick,回调队列的第一个事件则会在下一个tick中被执行,第二个事件会在下下个tick中...这样依次执行。

而作业队列则是位于当前tick的最尾部,在当前tick中添加的微任务都不会留到下一个tick,而是在tick的尾部触发执行。

一个事件循环中,在执行队列里的任务执行完毕以后,会有一个单独的步骤,叫 Perform a microtask checkpoint,即执行微任务检查点。这个操作是检查作业队列中是否有微任务,如果有,便将作业队也会当作执行队列来继续执行,完毕后将执行队列置空。

所以,这里我们就可以确定的说:同一个执行队列产生的微任务总是会在宏任务之前被执行

那么,我们现在回答第三点开始提出来的问题,宏任务和微任务的意义区别在哪呢?

个人的理解是宏任务是能够在宿主环境的协助下,通过回调队列来完成异步操作,微任务则是在宏任务执行前,进行某些操作,告诉 Javascript 引擎在处理完当前执行队列后,尽快地执行我们的代码。

4.关于requestAnimationFrame

起初我对requestAnimationFrame的定义是宏任务,因为在测试requestAnimationFrame的时候我用了下面这段代码

const testElement = document.getElementById('testElement')
setTimeout(() => {
  console.log(performance.now(), 'settimeout')
}, 0)
requestAnimationFrame(() => {
  console.log(performance.now(),
 'requestAnimationFrame')
})
var observer = new MutationObserver(() => {
  console.log('MutationObserver')
});
observer.observe(testElement, {
 childList: true 
})
const div = document.createElement('div')testElement.appendChild(div)
new Promise(resolve => {
  console.log('promise')  resolve()
}).then(() => console.log('then'))
console.log(performance.now(), 'global')

在浏览器的输出会有差异,多次运行以后出现了两种结果

第一种是:


另外一个是:


起初我将requestAnimationFrame归到宏任务中,原因是它绝大多数都会在setTimeout回调执行之后才执行。并将这个结果解释为是由于浏览器在执行渲染的时候,每次执行的时间会有差异,所以导致requestAnimationFrame和setTimeout被压入回调回咧的时机不一致,也就导致了回调的时间不一致。

但这种强行解释还是站不住脚,嘿嘿,我等作为一名立志成为优秀 Programer 的有志青年,肯定还是需要找找论据。      ----____是南风

后来在查了一些资料,在看了这篇规范文档后,发现在一个事件循环的tick中是包含浏览器渲染过程的,requestAnimationFrame的触发是在浏览器重绘之前,MDN文档介绍如下:

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

所以,requestAnimationFrame的回调时机也是在当前的tick中,所以不属于宏任务,但也不是微任务,排在微任务之后。


当然,这个问题如果有大佬可以赐教,欢迎评论区留言。/手动撒花 /手动撒花

5.总结

微任务与宏任务是我在处理一个七星瓢虫的时候,偶然接触到的知识。整理完这份文章,感觉对 JavaScript 事件循环的理解又深入了一点。也希望对阅读到这篇文档的你能产生帮助,哈哈。

“任何可以用JavaScript来写的应用,最终都将用JavaScript来写” --- 阿特伍德定律

附补充

  1. Node环境下宏任务:
    1. I/O 操作
    2. setTimeout
    3. setInterval
    4. setImmediate
  2. Node环境下微任务:
    1. Promise
    2. process.nextTick
  3. 增加了while循环进行延时,你能对事件循环感受的更清楚:
console.log('1')
setTimeout(() => {
  console.log('2')
})
new Promise((resolve, rejects) => {
  console.log('3')
  resolve()
}).then(() => {
  let i = 0
  while(i < 1000000000) {
    i++
  }
  console.log('4')
})
let i = 0
while(i < 1000000000) {
  i++
}
console.log(5)