你连微任务和宏任务都搞不明白,你咋学异步啊?

406 阅读6分钟

前言

前两天写了关于异步的文章,从一个小白的角度简单聊了聊关于异步的问题,还手撕了一下promise。然而当我肤白貌美,可爱迷人,三十六D的学妹看完之后,发出了令我眼前一黑的灵魂拷问——“学长,什么是微任务和宏任务啊?”

正文

编译器的“浪子回头”

要想简单又快捷地了解清楚什么是微任务和宏任务,还得从异步讲起。都知道JS是单线程语言,所以对一些需要耗时的代码做处理。

 

setTimeout(() => {
    console.log('江西省分晏');
}, 0);

console.log('我是彭于晏');

就上面这个demo来说,大家都知道是先输出“我是彭于晏”,然后再输出“江西省分晏”。因为编译器在遇到定时器的时候并不会死等,而是跳过去执行后面的代码,然后等时间到了再回过头执行定时器里面的代码,甚至是不耗时的定时器也是如此。讲到这里,大家应该都能理解,但是,问题来了,编译器凭什么回头?

微任务与宏任务

 

宏任务(Macrotasks)

宏任务通常涉及宿主环境(如浏览器或Node.js)的任务调度,执行完成后可能触发界面渲染或其他宏任务。一些常见的宏任务包括:

  • Script Execution:整个JavaScript脚本的执行就是一个宏任务。例如在一份html文件中,包含的所有代码都会被视作一个宏任务。

  • setTimeout & setInterval:使用setTimeout和setInterval设置的延迟和周期性执行的函数。就比如我们在前面提到的demo中的定时器就是

  • UI Rendering:浏览器对DOM的修改进行渲染。

  • I/O:文件读写等输入输出操作。

  • Ajax请求:异步的HTTP请求,如使用XMLHttpRequest或fetchAPI。

  • Event Handling:处理如点击、加载等DOM事件。

  • setImmediate(Node.js独有):在I/O循环的下一次迭代开始时执行,用于Node.js环境。

微任务(Microtasks)

微任务通常在当前执行栈中宏任务执行完毕后立即执行,且总是在下一个宏任务之前执行完毕,这使得微任务具有更高的优先级。常见的微任务包括:

  • Promise身上的回调函数:包括.then(), .catch(), 和 .finally()的回调。

  • Async/Await:基于Promise的异步函数的执行结果处理,在后面会详细解释。

  • process.nextTick(Node.js独有):在当前执行栈的末尾,但下一个事件循环迭代开始前执行,常用于Node.js中快速插入回调。

  • MutationObserver:监听DOM变化的回调。

浏览器中的event-loop

相信大家对JS的调用栈应该是不陌生的,浏览器中的event-loop和调用栈可以说有着异曲同工之妙。在浏览器中,时间循环的顺序如下:

  1. 执行同步代码(这属于宏任务)

  2. 当执行栈(调用栈)为空,查询是否有异步代码需要执行

  3. 执行微任务

  4. 如果有需要,会渲染页面

  5. 执行宏任务(这也叫下一轮的 event-loop 的开启)

  还是拿刚刚那个例子来说,如果我把它放进html中,当html读到script标签的那一刻,就开启了一轮属于js的宏任务。然后输出 “我是彭于晏”之后,查询这里面是否有异步代码(注意这里的定时器属于宏任务),然后再查询是否有微任务(同样没有)。执行到这里,会查看是否需要渲染界面(当然这里同样不需要)。最后,为刚刚的定时器开启一轮新的宏任务。进行与刚刚相同的步骤,输出“江西省分晏”之后,定时器开启的那一轮宏任务结束,script开启的第一轮宏任务也结束。  

上面讲到的就是一个最简单的宏任务与微任务的执行,为了各位读者老爷能够更好地理解时间循环的执行效果,我这里再用一个新的demo来解释

 

console.log('script start')
async function async1() {
    await async2()

    console.log('async1 end')
}
async function async2() {
    console.log('async2 end')
}
async1()
setTimeout(function () {
    console.log('setTimeout')
}, 0)
new Promise(resolve => {
    console.log('Promise')
    resolve()
})
    .then(function () {
        console.log('promise1')
    })
    .then(function () {
        console.log('promise2')
    })
console.log('script end')
  1. 首先,js文件开始执行,开始第一次宏任务(之后简称宏1)第一行输出“script start”,这一步应该没有任何问题

  2. 第二到第九行分别是async1和async2两个函数的声明,也是同步代码,没有影响

  3. 第10行开始async1的调用,发现首先要await   async2的执行,再看async2,直接打印“async2  end”,是同步代码,非异步。所以第二步输出的是“async2  end”

  4. 但是需要注意的是,虽然async2的执行不耗时,但是在后面的所有代码全部会被推入微任务队列,所以即使后面的“async1  end”也是个同步代码,但是并不会紧跟在“async2  end”后面输出。

  5. 11到14行是定时器,不予理会。但是js编译器会记住这里有个定时器。

  6. 14到17行是promise对象,在里面直接打印了“Promise”,是同步代码。Resolve只是为了后面then方法的执行。

  7. Promise对象后面的两个then方法都是微任务,全部推入微任务队列

  8. 输出最后一行的“script  end”

  9. 从这里开始,宏1里面的同步代码全部执行完毕,随后开始从上到下执行宏1里面的微任务。依次是打印“async1  end”,然后打印两个then方法的“promise1”和“promise2”。

  10. 到这里宏1的任务就是执行完毕了,因为宏1不涉及页面渲染,并且JS编译器还记着前面的定时器,所以接下来就是由定时器开启的第二次宏任务(以下简称宏2)。

  11. 宏2里面就简单了 ,就一个打印“setTimeout”。随后,宏2结束。至此整份JS文件执行完毕。

01.png

总结

对于JS中宏任务与微任务的理解能够很大程度上帮我们了解为什么JS作为一门单线程的语言却能够执行异步操作。同样的,能完全理解宏任务与微任务的执行也对编写符合要求的代码有很大帮助,希望这篇文章能够对各位有所帮助。最后,祝各位读者老爷0 waring(s),0 error(s)。