Js中的宏任务&微任务/async & await/Promise & then

Js中的宏任务&微任务/async & await/Promise & then

宏任务与微任务执行顺序

Js是一种单线程语言,只有一条通道,那么在任务多的情况下,就会出现堵塞,必须通过异步来解决。

最开始没有微任务的时候,就只有“任务”(tasks),后来有了Promise,就出现了“微任务”(microtasks),原来的任务为了与“微任务”作为对比,就被叫成了“宏任务”(macrotasks),当然在英文中一般还是叫“任务”(tasks),所以下面我在写宏任务时,一般都写成“(宏)任务”表示这个“宏”字可要可不要。

Js的执行顺序是这样的:

执行顺序:(宏)任务1 -> GUI渲染 ->-> (宏)任务2 -> GUI渲染 ->-> ……

也就是说,每执行完一次任务,GUI渲染一次,问题的关键在于,每一个“任务”(或者说“宏任务”)里,都有可能包含“微任务”,这些“微任务”是在执行该“宏任务”时产生的,这些微任务会在本次宏任务内的所有同步任务执行结束后,才会被执行,而且如果微任务是动态加入的,它也会被立刻执行,因为无论如何它都属于“本次宏任务”。

一个任务(或者叫“宏任务”)执行期间,有可能产生两种异步任务,异步宏任务:

  • 一种是异步宏任务,比如当前任务中有setTimeout/setInterval之类的语句,就会产生一个异步宏任务,它会被添加到宏任务队列中,等本次宏任务结束后,才会被执行;
  • 另一种是异步微任务,它从属于本次宏任务,它会在本次宏任务所有“同步任务”运行结束后,再执行,由于它本质上属于“本次宏任务”,所以它一定比下一次宏任务早执行;

微任务产生的本质原因是因为“异步”,有些任务不方便马上执行,所以就只能先“等待”,这部分“等待”执行的任务就是微任务(当然有些是宏任务,像setTimeout/setInterval),微任务队列中的微任务,被事件触发线程管理,事件触发线程发现某个微任务已经完成了,它就会把它加入到“待执行微任务队列”中,等执行栈中的所有同步任务执行结束,“待执行微任务队列”中的微任务就会被读取并按顺序执行。特别注意这个“任务队列”并不是宏任务队列,它还是在本次宏任务内部的。

宏任务与微任务执行顺序图解

如下图所示,一个宏任务中可能包括多个同步任务和微任务

常见宏任务与微任务

宏任务

  • 主代码块
  • setTimeout
  • setInterval
  • setImmediate () -> (仅Nodejs)
  • requestAnimationFrame () -> (仅浏览器)

微任务

  • Promise.then()
  • catch
  • finally
  • Object.observe
  • MutationObserver
  • process.nextTick () -> (仅Nodejs)

关于主代码块是否为“(宏)任务”的问题:有人认为主代码块属于(宏)任务,有人认为主代码块不属于任务。

认为主代码块不属于任务的人认为,主代码块本来就是从上到下执行,除了主代码块外,其它的代码块执行入口都是“回调函数”,而回调函数肯定都不会立刻被执行,所以会被暂时保存在一个地方,保存起来“等待执行”的才能叫“任务”,而主代码块的入口并不是回调函数,所以不能称之为“任务”。

当然,另一种思路是,我们可以把“主代码块”想像成有一个隐形的“大函数”装着,这时可以把这个“大函数”认为是一个(宏)任务,并且它肯定是第一个宏任务。

经典面试题(腾讯)

async function async1() {
    console.log("async1 start");
    await async2();
    console.log("async1 end");
}

async function async2() {
    console.log("async2");
}

console.log("script start");

setTimeout(function() {
    console.log("setTimeout");
}, 0);

async1();

new Promise(function(resolve) {
    console.log("promise1");
    resolve();
}).then(function() {
    console.log("promise2");
});

console.log("script end");

// 输出:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
  • 1、首先输出“script start”,这个没什么好说的,毕竟前面都是函数的定义,还没有被调用,自然也就不可能执行这些函数里面的语句;
  • 2、输出“script start”后,往下走就是setTimeout了,由于setTimeout是异步(宏)任务,它会放到下一次事件循环中执行(本次事件循环就是整个代码块),并不是0秒就立刻执行,而是指任务队列空闲的时候马上(等待0秒)就执行;
  • 3、然后往下执行async1(),发现await async2()需要等待Promise完成后才能往下执行,那怎么实现这个功能呢?实现方式就是暂时把async1()函数内await async2()后面的语句依次添加到本次任务的“微任务队列”中,等本次任务都执行完了,再来执行本次任务产生的微任务。为什么要这么做呢?因为await async2()后面的语句都要等待await async2()后返回结果后才能执行,所以只能把它们先搬到另一个地方呀,不然它们就会立刻执行了,而立刻执行的话,就体现不出await的功能了,因为await意思就是要等待返回结果后才往下执行。事实上async修饰的函数内部await之后的语句,就是Promise对象中then()中的语句,因为async/await本质上就是Promise/then的一种方便的简化写法;
  • 4、上一步把async1()函数中的await async2()后面的语句都搬到另一个任务队列(微任务队列)后,然后开始执行async1()函数中剩下的语句,输出“async1 start”,调用await async2(),输出“async2”;
  • 5、特别注意,async1()函数中的await async2()执行完,相当于async2()已经返回了(async2()函数中没有return,相当于return Promise.resolve(undefined)),既然已经返回了,是不是会继续往下执行“async1 end”呢?No!“async1 end”在开始调用async1()时就被搬到微任务队列了,而在同一个(宏)任务中,同步任务未执行完时,是不会执行微任务的,所以await async2()执行之后,相当于整个async1()已经执行结束了;
  • 5、async1()执行结束后,继续往下,遇到Promise,由于Promise的非then部分属于同步任务,所以会被执行,输出“promise1”,然后Promise的then是微任务,所以会被加入微任务队列,注意,前面第3步的时候加入了一个微任务(即“async1 end”),现在又加了一个,所以微任务队列里就有两个微任务了,由于是队列,队列是先进先出的,所以待会儿执行微任务时,“async1 end”先执行,而Promise的then后执行;
  • 然后继续往下,输出“script end”,整个代码块运行结束,然后回头去查看该代码块运行过程中产生的微任务队列,发现微任务队列中两个微任务都可以执行了,因为async2()没有return语句,相当于return Promise.resolve(undefined),只要是resolve了,说明数据已经返回了,所以就可以往下执行“async1 end”了,然后是第二个任务,由于Promise中调用了resolve(),所以这个微任务也可以执行了,所以它被执行,打印“promise2”;
  • 最后本轮事件循环(Event-Loop)结束(即本次宏任务结束),进入下一轮事件循环(即下一个宏任务),发现有个setTimeout的回调函数在那等待执行,然后发现它设置的等待时间是0,于是马上执行了,输出“setTimeout”。

这里有一个按步骤执行的Js-Event-Loop,点开后如果自动跳转到1443端口,请手动改为443端口后访问。

参考:
微任务/宏任务和同步/异步之间的关系
「硬核JS」一次搞懂JS运行机制(推荐)
async函数是个微任务
Js中宏任务和微任务的简单理解
重学js — 宏任务与微任务的理解
Tasks, microtasks, queues and schedules
Macro task, micro task, async, await principle of interview

打赏

订阅评论
提醒
guest

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x

扫码在手机查看
iPhone请用自带相机扫
安卓用UC/QQ浏览器扫

Js中的宏任务&微任务/async & await/Promise & then