Js中的宏任务&微任务/async & await/Promise & then
Table of Contents
宏任务与微任务执行顺序
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