宏任务和微任务的文章很多,但是很多比较易理解的文章讲的不完全,而讲的完全的文章基础差又不好上手,本篇文章的目的是即容易理解又可以带你彻底搞懂这个知识点。
一、快速掌握
首先需要明确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)。
在代码中的执行顺序,同步任务>微任务>宏任务
仅通过上面的文字可能不容易理解,让我们配合几道题来理解一下:
牛刀小试: 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
同步任务:152
微任务:3
宏任务:4
这里只有一个需要注意的地方就是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
同步任务:1562
微任务:73
宏任务:4
这里需要注意的地方是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
同步任务:246
微任务:357
宏任务:1
这里需要注意的地方是await,await后面一般跟一个Promise对象,如果后面不是Promise对象就会被转成Promise对象,await会暂停当前async function的执行,等待await表达式后面的Promise处理完成后才会继续执行。
最后再来一道UI事件相关的!
下面这道题如果用户在页面加载后就去点击按钮,console.log()的输出顺序又是怎样的呢?
document.onclick = function() {
console.log("1");
};
console.log(2);
// 3秒之后才会提交到任务队列中
setTimeout(function() {
console.log(3);
}, 3000);
setTimeout(() => {
console.log(4);
}, 0);
答案:2413
同步任务:2
微任务:41
宏任务:3
这道题就不做过多解释了,至此,想必在应用层面,你已经掌握了宏任务和微任务了,如果你只是想会使用的话到这里就可以退出了。
二、常见问题
1.为什么说script是一个宏任务?
这里说的script是<script></script>脚本,这被当作是一个宏函数,可能有些人不太好理解,我们来看个例子:
请写出下面这个例子中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
这里将script脚本作为宏任务,执行顺序如下:
- 将宏任务script1和script2加入event table,并按顺序注册到event queue,等待主线程拉取
- 主线程拉取第一个宏任务script1,先执行script1中的同步任务,输出
1-1、1-3、1-5,然后执行微任务,输出1-4,在执行script1的过程中发现settimeout宏任务,将settimeout加入到event table,并在大约10毫秒后推入event queue - 此时,script1中的同步任务和微任务都已经执行完毕了,主线程去event queue拉取先注册的宏任务script2,
- 执行script2中的同步任务,输出
2-1、2-3、2-5,然后是微任务,输出2-4,在执行script2的过程中发现settimeout宏任务,将settimeout加入到event table,并在大约10毫秒后推入event queue - script2中的同步任务和微任务都已经执行完毕了,主线程去event queue拉取注册靠前的宏任务script1中的settimeout,输出
1-2,settimeout中没有微任务需要执行 - 主线程去event queue拉取宏任务script1中的settimeout,输出
2-2
这里对上面提到的event table和event queue做个补充解释-JS的执行机制:
-
首先判断JS是同步还是异步,同步就进入主进程,异步就进入event table
-
异步任务在event table中注册函数,当满足触发条件后,被推入event queue
-
同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主进程中。
满足触发条件是指什么呢?
以上面的settimeout为例:settimeout(()=>{},10),表示10豪秒后可以执行回调函数,这里满足触发的条件就是10毫秒后。
2.宏任务与微任务的理解和差异
很多人说是先微再宏,这样说没问题,但是我觉得这样不太好理解。
这里多提一嘴我个人对宏任务和微任务的理解,我将宏任务比作一个大型生物(如:一颗参天大树),在这颗大树上可以生长存活其他的动物(宏任务)和寄生于这颗大树的动植物(微任务),一个名为主线程的人提着一把刀要灭绝这里(好像有点邪恶哈...)。首先刀砍大树(刀名: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
同步任务:125
微任务:3
宏任务:4
3秒后console.log(4)被输出,因为在执行promise微任务耗费了3秒
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>
答案:
1.弹出框'同步0'(同步任务)
2.弹出框"promise0"(微任务)
3.弹出框"settimeout1"(settimeout1宏任务)
4.页面渲染:页面中的'元素div1'被渲染展示
5.弹出框"promise1"(settimeout1宏任务中的微任务)
6.弹出框"settimeout2"(settimeout2宏任务)
7.页面渲染:页面中的文字'元素div1'=>'元素div2'
这个例子之所以用alert,是因为alert会阻塞线程,方便我们观察页面渲染,这个例子主要想表达:
每个宏任务执行完毕并清空里面的微任务后,会进行一次页面渲染
最后附赠一张网上找的事件循环的图,大家可以看图再理解一下,哪里不明白也可以私信或者评论问我