macrotask(宏任务):主代码块、setTimeout、setInterval等
microtask(微任务):Promise、process.nextTick等
下面通过【今日头条】这道面试题详细说明一下:
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' )
先画一个通用任务表(后面的步骤就是给表插值的过程,执行顺序是从上至下,从左往右,先同步后异步)
宏任务1 | 宏任务2 | 宏任务3 | ... | 宏任务n |
---|---|---|---|---|
微任务1.1 | 微任务2.1 | 微任务3.1 | ... | 微任务n.1 |
微任务1.2 | 微任务2.2 | 微任务3.2 | ... | 微任务n.2 |
微任务1.3 | 微任务2.3 | 微任务3.3 | ... | 微任务n.3 |
... | ... | ... | ... | ... |
步骤详解:
1、咱们整道面试题就是宏任务1,async1()及async2()函数声明先不管,直接看第一步执行的代码:console.log( 'script start' )
,没有疑问,同步微任务。
面试题主代码块 | 宏任务2 | 宏任务3 | ... | 宏任务n |
---|---|---|---|---|
console.log( 'script start' ) | 微任务2.1 | 微任务3.1 | ... | 微任务n.1 |
微任务1.2 | 微任务2.2 | 微任务3.2 | ... | 微任务n.2 |
微任务1.3 | 微任务2.3 | 微任务3.3 | ... | 微任务n.3 |
... | ... | ... | ... | ... |
2、然后到setTimeout
,一个新的宏任务,即宏任务2,其中函数执行代码console.log( 'setTimeout' )
及该宏任务中的微任务。
面试题主代码块 | setTimeout | 宏任务3 | ... | 宏任务n |
---|---|---|---|---|
console.log( 'script start' ) | console.log( 'setTimeout' ) | 微任务3.1 | ... | 微任务n.1 |
微任务1.2 | 微任务2.2 | 微任务3.2 | ... | 微任务n.2 |
微任务1.3 | 微任务2.3 | 微任务3.3 | ... | 微任务n.3 |
... | ... | ... | ... | ... |
3、接下来是函数调用async1()
,console.log( 'async1 start' )
同步代码进入宏任务1。
面试题主代码块 | setTimeout | 宏任务3 | ... | 宏任务n |
---|---|---|---|---|
console.log( 'script start' ) | console.log( 'setTimeout' ) | 微任务3.1 | ... | 微任务n.1 |
console.log( 'async1 start' ) | 微任务2.2 | 微任务3.2 | ... | 微任务n.2 |
微任务1.3 | 微任务2.3 | 微任务3.3 | ... | 微任务n.3 |
... | ... | ... | ... | ... |
4、随后执行await async2()
语句,这里会先调用async2()中的同步代码,然后阻塞async1()的执行(直到该宏任务中所有同步代码执行完毕后再继续),具体原因可以参见我之前写的:async与await总结,所以async2()中同步代码console.log( 'async2' )
进入宏任务1。
所以这里先把宏任务1中待执行的异步代码console.log( 'async1 end' )
放最后面(其实是时间循环机制会在同步代码执行完毕即主线程空闲时,遍历一遍异步微任务,看是否有可执行的异步任务,有的话就推入主线程,我们人工怕遗留,所以先放后面):
面试题主代码块 | setTimeout | 宏任务3 | ... | 宏任务n |
---|---|---|---|---|
console.log( 'script start' ) | console.log( 'setTimeout' ) | 微任务3.1 | ... | 微任务n.1 |
console.log( 'async1 start' ) | 微任务2.2 | 微任务3.2 | ... | 微任务n.2 |
console.log( 'async2' ) | 微任务2.3 | 微任务3.3 | ... | 微任务n.3 |
... | ... | ... | ... | ... |
console.log( 'async1 end' ) | ... | ... | ... | ... |
5、下面是执行new Promise(),Promise构造函数是直接调用的同步代码,所以将console.log( 'promise1' )
加入宏任务1,但其中resolve()的执行为异步代码(实际为promise.then()的执行),所以将console.log( 'promise2' )
依次放最后面:
面试题主代码块 | setTimeout | 宏任务3 | ... | 宏任务n |
---|---|---|---|---|
console.log( 'script start' ) | console.log( 'setTimeout' ) | 微任务3.1 | ... | 微任务n.1 |
console.log( 'async1 start' ) | 微任务2.2 | 微任务3.2 | ... | 微任务n.2 |
console.log( 'async2' ) | 微任务2.3 | 微任务3.3 | ... | 微任务n.3 |
console.log( 'promise1' ) ) | 微任务2.4 | 微任务3.4 | ... | 微任务n.4 |
... | ... | ... | ... | ... |
console.log( 'async1 end' ) | ... | ... | ... | ... |
console.log( 'promise2' ) | ... | ... | ... | ... |
6、最后是console.log( 'script end' )
同步代码的执行,放入宏任务1,同步之后,异步之前。
面试题主代码块 | setTimeout | 宏任务3 | ... | 宏任务n |
---|---|---|---|---|
console.log( 'script start' ) | console.log( 'setTimeout' ) | 微任务3.1 | ... | 微任务n.1 |
console.log( 'async1 start' ) | 微任务2.2 | 微任务3.2 | ... | 微任务n.2 |
console.log( 'async2' ) | 微任务2.3 | 微任务3.3 | ... | 微任务n.3 |
console.log( 'promise1' ) ) | 微任务2.4 | 微任务3.4 | ... | 微任务n.4 |
console.log( 'script end' ) | ... | ... | ... | ... |
console.log( 'async1 end' ) | ... | ... | ... | ... |
console.log( 'promise2' ) | ... | ... | ... | ... |
7、最后按照从上至下,从左往右的顺序执行就可以得到最终的结果了。
最终的打印顺序:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
参考资料:
1、8张图看清 async/await 和 promise 的执行顺序(原文中async1 end和promise2弄反了,需要注意):https://segmentfault.com/a/1190000017224799?_ea=5345525