由浅入深-宏任务与微任务

525 阅读7分钟

宏任务和微任务的文章很多,但是很多比较易理解的文章讲的不完全,而讲的完全的文章基础差又不好上手,本篇文章的目的是即容易理解又可以带你彻底搞懂这个知识点。

一、快速掌握

首先需要明确js是一门单线程语言,这意味着执行js中的任务只能按顺序一件一件来执行,只有当上一个任务结束后,才会去执行下一个任务,不可同时执行多个任务。

Javascript语言将任务的执行模式分成两种:同步任务和异步任务(对于这两种任务这里不再赘述,不了解的小伙伴随意百度便知),宏任务和微任务都属于异步任务

宏任务script(理解为外层同步代码)setTimeout/setIntervalpostMessagerequestAnimationFrameMessageChannelUI rending/UI事件setImmediate与I/O(Node.js环境)

微任务Promise.then()await后面的代码MutaionObserver(html5新特性)proxyprocess.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脚本作为宏任务,执行顺序如下:

  1. 将宏任务script1和script2加入event table,并按顺序注册到event queue,等待主线程拉取
  2. 主线程拉取第一个宏任务script1,先执行script1中的同步任务,输出1-11-31-5,然后执行微任务,输出1-4,在执行script1的过程中发现settimeout宏任务,将settimeout加入到event table,并在大约10毫秒后推入event queue
  3. 此时,script1中的同步任务和微任务都已经执行完毕了,主线程去event queue拉取先注册的宏任务script2,
  4. 执行script2中的同步任务,输出2-12-32-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 tableevent 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会阻塞线程,方便我们观察页面渲染,这个例子主要想表达:

每个宏任务执行完毕并清空里面的微任务后,会进行一次页面渲染

最后附赠一张网上找的事件循环的图,大家可以看图再理解一下,哪里不明白也可以私信或者评论问我

ca805adde7a54f5aa9ffd054b290fd9e.png