宏任务和微任务的文章很多,但是很多比较易理解的文章讲的不完全,而讲的完全的文章基础差又不好上手,本篇文章的目的是即容易理解又可以带你彻底搞懂这个知识点。
## 一、快速掌握
首先需要明确js是一门单线程语言,这意味着执行js中的任务只能按顺序一件一件来执行,只有当上一个任务结束后,才会去执行下一个任务,不可同时执行多个任务。
Javascript语言将任务的执行模式分成两种:同步任务和异步任务(对于这两种任务这里不再赘述,不了解的小伙伴随意百度便知),`宏任务和微任务都属于异步任务`。
**宏任务**:`script(理解为外层同步代码)`、`setTimeout/setInterval`、`postMessage`、`requestAnimationFrame`、`MessageChannel`、`UI rending/UI事件`、`setImmediate与I/O(Node.js环境)`。
**微任务**:`Promise.then()`、`await后面的代码`、 `MutaionObserver(html5新特性)`、`proxy`、`process.nextTick(Node.js)`。
在代码中的执行顺序,`同步任务>微任务>宏任务`
仅通过上面的文字可能不容易理解,让我们配合几道题来理解一下:</br>
**牛刀小试**: consloe.log()会输出什么呢
```
console.log(1)
setTimeout(() => {
console.log(4)
}, 0);
new Promise(resolve=>{
console.log(5)
resolve(3)
}).then(res=>{
console.log(res)
})
console.log(2)
```
`答案:15234`</br>
`同步任务:152`</br>
`微任务:3`</br>
`宏任务:4`</br>
这里只有一个需要注意的地方就是Promise,`.then方法才是微任务`,console.log(5)是同步任务
哈哈,先同步任务再微任务最后宏任务,这道题一定难不住聪明的你吧,提升一下难度!
```
console.log(1);
setTimeout(() => {
console.log(4);
}, 0);
new Promise((resolve) => {
console.log(5);
resolve(3);
new Promise((resolve) => {
console.log(6);
resolve(7);
}).then((res) => {
console.log(res);
});
}).then((res) => {
console.log(res);
});
console.log(2);
```
`答案:1562734`</br>
`同步任务:1562`</br>
`微任务:73`</br>
`宏任务:4`</br>
这里需要注意的地方是Promise.then()这个微任务放到事件队列的顺序,`并不是执行到resolve()方法就会将这个微任务加入到事件队列,而是要执行到.then方法的才会被加入事件队列`,所以6在前而7在后(ps:事件队列可以理解为要被执行的微任务会被加入到事件队列,先进先出)
这道题你答对了吗?再看下一道!
```
setTimeout(() => {
console.log(1);
}, 0);
async function test1() {
console.log(2)
}
async function test2() {
await test1()
console.log(3)
}
test2()
new Promise((resolve) => {
console.log(4);
resolve(5);
new Promise((resolve) => {
console.log(6);
resolve(7);
}).then((res) => {
console.log(res);
});
}).then((res) => {
console.log(res);
});
```
`答案:2463751`</br>
`同步任务:246`</br>
`微任务:357`</br>
`宏任务:1`</br>
这里需要注意的地方是await,`await后面一般跟一个Promise对象,如果后面不是Promise对象就会被转成Promise对象,await会暂停当前async function的执行,等待await表达式后面的Promise处理完成后才会继续执行。`
最后再来一道UI事件相关的!</br>
下面这道题如果用户在页面加载后就去点击按钮,console.log()的输出顺序又是怎样的呢?
```
document.onclick = function() {
console.log("1");
};
console.log(2);
// 3秒之后才会提交到任务队列中
setTimeout(function() {
console.log(3);
}, 3000);
setTimeout(() => {
console.log(4);
}, 0);
```
`答案:2413`</br>
`同步任务:2`</br>
`微任务:41`</br>
`宏任务:3`</br>
这道题就不做过多解释了,至此,想必在应用层面,你已经掌握了宏任务和微任务了,如果你只是想会使用的话到这里就可以退出了。
## 二、常见问题
#### 1.为什么说script是一个宏任务?
这里说的script是`<script></script>`脚本,这被当作是一个宏函数,可能有些人不太好理解,我们来看个例子:</br>
请写出下面这个例子中console.log()的输出顺序
```
<!-- 脚本 1 -->
<script>
console.log('1-1')
setTimeout(() => console.log('1-2'), 0)
new Promise((resolve, reject) => {
console.log('1-3')
resolve('1-4')
}).then((res) => {
console.log(res)
})
console.log('1-5')
</script>
<!-- 脚本 2 -->
<script>
console.log('2-1')
setTimeout(() => console.log('2-2'), 0)
new Promise((resolve, reject) => {
console.log('2-3')
resolve('2-4')
}).then((res) => {
console.log(res)
})
console.log('2-5')
</script>
```
`答案:1-1、1-3、1-5、1-4、2-1、2-3、2-5、2-4、1-2、2-2`</br>
这里将`script脚本`作为宏任务,执行顺序如下:
1. 将宏任务script1和script2加入event table,并按顺序注册到event queue,等待主线程拉取
2. 主线程拉取第一个宏任务script1,先执行script1中的同步任务,输出`1-1`、`1-3`、`1-5`,然后执行微任务,输出`1-4`,在执行script1的过程中发现settimeout宏任务,将settimeout加入到event table,并在大约10毫秒后推入event queue
3. 此时,script1中的同步任务和微任务都已经执行完毕了,主线程去event queue拉取先注册的宏任务script2,
4. 执行script2中的同步任务,输出`2-1`、`2-3`、`2-5`,然后是微任务,输出`2-4`,在执行script2的过程中发现settimeout宏任务,将settimeout加入到event table,并在大约10毫秒后推入event queue
5. script2中的同步任务和微任务都已经执行完毕了,主线程去event queue拉取注册靠前的宏任务script1中的settimeout,输出`1-2`,settimeout中没有微任务需要执行
6. 主线程去event queue拉取宏任务script1中的settimeout,输出`2-2`
这里对上面提到的`event table`和`event queue`做个补充解释-`JS的执行机制`:
- 首先判断JS是同步还是异步,同步就进入主进程,异步就进入event table
- 异步任务在event table中注册函数,当满足触发条件后,被推入event queue
- 同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主进程中。</br>
满足触发条件是指什么呢?</br>
以上面的settimeout为例:`settimeout(()=>{},10)`,表示10豪秒后可以执行回调函数,这里满足触发的条件就是10毫秒后。
#### 2.宏任务与微任务的理解和差异
很多人说是先微再宏,这样说没问题,但是我觉得这样不太好理解。</br>
这里多提一嘴我个人对宏任务和微任务的理解,我将宏任务比作一个大型生物(如:一颗参天大树),在这颗大树上可以生长存活其他的动物(宏任务)和寄生于这颗大树的动植物(微任务),一个名为主线程的人提着一把刀要灭绝这里(好像有点邪恶哈...)。首先刀砍大树(刀名:js引擎线程),当大树倒下了,第一时间死掉的是寄生于这颗大树的生物(微任务),然后依次再轮到其他的动物(宏任务),循环往复将这颗大树身上所有的生物都干掉!
###### 微任务宏任务差异表
| | 宏任务 | 微任务 |
| --- | --- |--- |
| 是否重新渲染页面 | 会|不会|
| 是否需要其他异步线程的支持 | 需要|不需要|
| 宏任务与微任务发起者 | 宿主(node、浏览器)|js引擎|
| 具体事件 | script、setTimeout/setInterval、postMessage、MessageChannel、requestAnimationFrame、UI rending/UI事件、setImmediate与I/O(Node.js环境)|Promise.then()、await后面的代码、 MutaionObserver(html5新特性)、proxy、process.nextTick(Node.js) |
#### 3.宏任务与微任务产生的误差
直接上代码
```
console.log(1)
new Promise((resolve,reject)=>{
console.log(2)
resolve(3)
}).then(res=>{
console.log(res)
const targetTime = new Date().getTime()+3000
let isEnd = false
// 延时三秒
while(!isEnd){
const currentTime = new Date().getTime()
if (targetTime <= currentTime){
isEnd = true
}
}
})
setTimeout(() => console.log(4), 0)
console.log(5)
```
`答案:12534`</br>
`同步任务:125`</br>
`微任务:3`</br>
`宏任务:4`</br>
`3秒后console.log(4)被输出`,因为在执行promise微任务耗费了3秒</br>
setTimeout二个参数仅仅表示最少延迟时间,而非确切的等待时间,其他具有回调方法的宏任务也基本是这样。
即使是在异步任务中的做费时等的延迟操作,也会影响到同为异步任务的宏任务,在代码开发中一定要注意比较耗时的代码所产生的影响!
#### 4.通过一个例子加深了解
下面这个例子中`alert()`和`界面div的显示的变化`是如何顺序的呢?
```
<body>
<div class="demo"></div>
<script>
window.onload = function() {
const div = document.querySelector(".demo");
Promise.resolve().then(() => {
alert("promise0");
});
alert('同步0')
// 宏任务1
setTimeout(() => {
Promise.resolve().then(() => {
alert("promise1");
});
div.textContent = "元素div1";
alert("settimeout1");
}, 0);
// 宏任务2
setTimeout(() => {
div.textContent = "元素div2";
alert("settimeout2");
}, 0);
};
</script>
</body>
```
`答案:`</br>
`1.弹出框'同步0'(同步任务)`</br>
`2.弹出框"promise0"(微任务)`</br>
`3.弹出框"settimeout1"(settimeout1宏任务)`</br>
`4.页面渲染:页面中的'元素div1'被渲染展示`</br>
`5.弹出框"promise1"(settimeout1宏任务中的微任务)`</br>
`6.弹出框"settimeout2"(settimeout2宏任务)`</br>
`7.页面渲染:页面中的文字'元素div1'=>'元素div2'`</br>
这个例子之所以用alert,是因为alert会阻塞线程,方便我们观察页面渲染,这个例子主要想表达:
###### 每个宏任务执行完毕并清空里面的微任务后,会进行一次页面渲染</br>
最后附赠一张网上找的事件循环的图,大家可以看图再理解一下,哪里不明白也可以私信或者评论问我
![ca805adde7a54f5aa9ffd054b290fd9e.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2009bb97b3be41bdba8ab3e4f39bd9ed~tplv-k3u1fbpfcp-watermark.image?)