唠唠我对宏任务和微任务的理解

128 阅读5分钟

前言

我们熟知 JavaScript 是一门单线程的语言,只拥有一条通道,当它处于任务多的情况下会出现堵塞,然后就随之产生出了多线程,也就有了同步任务和异步任务。

同步任务:就是在主线程上排队执行的任务,只有在前一个任务执行完毕,才能执行下一个任务。不知道各位掘友在使用浏览器中的 alert 这个api的时候发现,我们必须点击了弹出框的确定或者取消两个按钮之后,后面的代码才能继续执行,这个就是因为我们调用 alert 的时候创建了一个同步任务,只有当它执行完毕之后,后一个任务才能接着执行。这些都是由 js 执行栈/回调栈 执行的。

异步任务:不进入主线程、而进入任务队列的任务,当主线程中的任务运行完成了之后,才会从任务队列中取出异步任务,放入主线程中执行。在这里我再举一个🌰,我们都使用过 setTimeout ,它就是一个很典型的异步任务,它会在指定的时间间隔后将函数推入 JavaScript 引擎的任务队列中,等待 JavaScript 的主线程空闲时被执行。

宏任务和微任务

宏任务和微任务都是异步任务,不过我们根据它们自身的不同之处进行了一个简单的区分,实际上它们都属于同一个队列。

宏任务:script、setTimeout、setInterval、postMessage、MessageChannel以及 Node.js 环境中的setImmediate。

微任务:Promise.then()/catch()、Object.observe、MutationObserver以及 Node.js 环境中的process.nextTick。

这里需要注意的一点是宏任务是由宿主环境发起的,也就是我们熟知的浏览器和 Node 等,而微任务则是由JS自带的引擎发起的。

在我上述举例的微任务中,Promise 本身是属于同步,只不过它里面的 then/catch 的回调函数是异步的微任务,当然还有 async/await 这个语法糖,它的出现解决了回调地狱的这个问题,大大的增加了我们代码的可读性。

任务执行的顺序

这是一个经常争执的问题,有人说宏任务先于微任务执行,也有人说微任务先于宏任务执行。

其实,我之前在上面提到过,script 标签本身就是一个大的宏任务,所以在执行任务的时候,肯定是执行 script 这个大的宏任务里面的代码,因此,我们可以认为宏任务是先于微任务的。

但是,当我们忽略掉 script 这个大的宏任务,仅仅只讨论 script 里包含的任务代码块,那么人物的执行顺序就一定是微任务优先于宏任务

在这里,我们就先忽略 script ,执行顺序就为:同步任务 → 微任务 → 宏任务

下面我们就来个案例演示详解一下任务执行的过程

console.log(1);
setTimeout(() => {
  console.log(2);
}, 0);

const p = new Promise((resolve, reject) => {
  console.log(3);
  resolve("success"); // 标记为成功
  console.log(4);
});

p.then((value) => {
  console.log(value);
});

console.log(5);

我们可以通过文章开头的理论解释,将上面代码分为同步任务、微任务、宏任务三块,哪些代码属于同步任务,哪些代码属于微任务,哪些代码属于宏任务,如图下所示:

image.png

将我们分析好的代码,分开放在相应的队列里,可得到以下代码执行的流程图示:

image.png

image.png

image.png

所以这个案例,最终得到的执行结果就是:1 3 4 5 “sucess” 2

详细讲解宏任务和微任务的区别

首先,宏任务和微任务的执行时机是不同的。

宏任务会在主线程执行完毕后被执行,而微任务会在主线程执行完毕之前立即执行。

其次,宏任务和微任务的优先级也是不同的。在 JavaScript 中,微任务的优先级比宏任务高,也就是说,如果微任务队列和宏任务队列中都有任务任务需要执行,微任务会有咸鱼宏任务执行。

另外,宏任务和微任务对于对于 JavaScript 事件循环的影响也是不同的。宏任务会触发事件循环,而微任务不会触发事件循环,而是在主线程上的同步任务执行完毕后立即执行。

此外,宏任务和微任务在实际应用中有着不同的用途。宏任务通常用于与浏览器渲染相关的任务,如 setTimeout和 requestAnimationFrame,而微任务通常用于异步操作,如 Promise.then和 async/await。

通过理解宏任务和微任务的区别,我们可以更好地优化代码的性能,例如避免在为任务中执行大量耗时的操作。

总之,宏任务和微任务是 JavaScript 中非常重要的概念,对于更好的理解 JavaScript 的执行机制和优化代码的性能是非常有帮助的。

下面我们再来一个案例来检验一下我们对宏任务与微任务的理解,

console.log(1);
setTimeout(() => {
  console.log(2);
  setTimeout(() => {
    console.log(3);
    setTimeout(() => console.log(4), 0);
    Promise.resolve().then(() => console.log(5));
  }, 0);
}, 0);
console.log(6);

解答:

  • 首先,输出 16,之后,第一层 setTimeout 中的回调函数执行,输出 2
  • 之后,执行第二层 setTimeout 中的回调函数,输出 3
  • 然后微任务 Promise.resolve() 执行并输出 5
  • 最后执行第二层 setTimeout 中的回调函数,输出 4

在这个例子中,可以看到微任务和宏任务在嵌套的 setTimeout 中被调度了,这就使得事件循环变得更加复杂。 具体的执行顺序如下:首先执行第一个 setTimeout 的回调函数,执行完之后把第二层 setTimeout 的回调函数和 Promise.resolve 放入微任务队列,紧接着执行第二层 setTimeout 的回调函数,执行完之后把第三层 setTimeout 的回调函数放入微任务队列,最后执行微任务队列中的任务,微任务 Promise.resolve 先于第三层 setTimeout 的回调函数执行。

总结

1.宏任务会在主线程执行完毕后被执行,而微任务会在主线程执行完毕之前立即执行。
2.宏任务会触发事件循环,而微任务不会触发事件循环,而是在主线程上执行完毕后立即执行。