到底宏任务跟微任务哪个先执行?
这里直接给出结论:JavaScript的事件循环中,宏任务比微任务先执行
这与我一直以来对这两个任务的执行顺序概念也是截然相反的,o(╥﹏╥)o
那么让我来回想一下为什么我会一直觉得微任务会比宏任务先执行呢?
首先应该是这道经常在各个文章上看到的面试题:
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('setTimeout0')
},0)
setTimeout(function(){
console.log('setTimeout3')
},3)
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
console.log('promise2')
}).then(function(){
console.log('promise3')
})
console.log('script end')
由于看到的那些文章中,并没有明确的说明宏任务跟微任务的执行先后顺序,那么在我不是很仔细地一个字一个字看的情况下,就理所当然得将作者的意思理解为了:
在代码执行过程中,JS主线程遇到异步任务时,会将微任务放到微任务队列,将宏任务放到宏任务队列,然后到主线程的执行栈空了以后,就去微任务队列看有没有微任务,有就执行微任务,没有就执行宏任务。
这样的话乍一想好像确实是微任务先执行, 然后微任务执行完了以后执行宏任务。但是。。。
在反过来去看一下哪些是宏任务,哪些是微任务:
宏任务包括:
script(重点),
setTimeout,
setInterval,
setImmediate,(node端的)
I/O,(node端的)
UI rendering
微任务包括:
Promise,
process.nextTick(node端的)
MutationObserver
(没有async await??,后面再说)
根据我画的重点,由于宏任务中包括了script,所以浏览器会首先执行一个宏任务,也就是script里的代码,接下来有异步代码的话才会先执行微任务。
那么我终于知道我一直以来的误会是怎么来的了,就是因为我忽略了第一个宏任务也就是script!
小小的总结一下
在JS的事件循环中的,首先是由script开启第一个宏任务,然后执行宏任务里的代码,遇到异步代码时,会将微任务放到微任务队列中,将宏任务放到宏任务队列中;等到这个宏任务中的同步代码执行完了以后,会将微任务队列的任务一个个入栈,知道清空微任务队列以后,本次循环结束(如果有必要会紧接着进行UI render),最后开启下一轮的事件循环。
注意点:
- 每次宏任务中产生的微任务都会在,本次宏任务执行完后, 下次宏任务执行前清空(全部执行完)
- Promise里的代码时同步执行的,只有resolve() 后的代码才是异步的(异步也不等于微任务或者宏任务哦)
- async 函数里的代码也是同步执行的,只有遇到await以后才会将主线程让出来,并且如果await后面如果是函数的调用,这个函数也会同步调用,但是await下面写的代码会等到其他同步代码执行完以后再执行
那么在回过来想前面的面试题。
首先:进入script开启第一个宏任务,然后碰到第一个同步代码 console.log('script start')
(函数的定义不用管他)输出 'script start'
;
然后往下碰到两个宏任务,丢进宏任务队列;
再往下调用async1
执行里面的同步代码console.log('async1 start')
,输出async1 start
然后碰到await
执行右边的函数调用async()
输出async2
返回一个Promise, 调用完以后让出主线程,执行后面的Promise
Promise执行里面的同步代码console.log('promise1')
,碰到resolve
把它丢到微任务队列,往下执行console.log('promise2')
再往下就是最后的同步代码console.log(script end)
, 接着执行异步代码(我们回到await让出线程的地方,执行console.log(async1 end)
), 异步代码执行完以后, 第一轮宏任务执行完
开始依次执行微任务,
微任务执行完, 执行宏任务队列里的任务, 即开启下一轮事件循环
所以上面题的顺序就是:
// script start=>async1 start=>async2=>promise1=>promise2=>script end=>async1 end=>promise3=>setTimeout0=>setTimeout3
// 由于浏览器实现关系,async1 end跟promise3的执行顺序可能有变化, 这个顺序是目前新浏览器的打印顺序
那么最后我们应该可以搞懂下面这道题的输出顺序了:
console.log('script start1')
setTimeout(() => {
console.log('setTimeout 1')
new Promise((resolve) => {
console.log('promise1 start')
resolve()
}).then(() => {
console.log('promise1 end')
new Promise(resolve => {
console.log('promise2 start')
resolve()
}).then(() => {
console.log("promise2 end")
})
})
})
async function async1() {
console.log('async1 start')
await async2();
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
async1().then(()=>{
console.log('async1 then');
});
new Promise((resolve) => {
console.log('promise3 start')
resolve()
}).then(res => {
console.log('promise3 end')
})
setTimeout(() => {
console.log('setTimeout2')
})
console.log('script end');
//输出顺序是这样的:
//script start1 => async1 start => async2 => promise3 start => script end => async1 end => primise3 end => async1 then => setTimeout 1 => promise1 start => promise1 end => promise2 start => promise2 after resolve => promise2 end => setTimeout2
我们再把async await的坑踩一下
async做了一件什么事情?
async将你的函数返回值转换为promise对象,不需要显式地返回promise对象,async关键字自动将函数的返回值变为promise对象。
await的作用
await关键字只能在带有async关键字的函数内部使用,在外部使用时会报错。await等待的是右侧的[表达式结果]
,如果右侧是一个函数,等待的是右侧函数的返回值,如果右侧的表达式不是函数则直接是右侧的表达式。await在等待时会让出线程阻塞后面的执行。await的执行顺序为从右到左,会阻塞后面的代码执行,但并不是直接阻塞await的表达式。
await之后如果不是promise,await会阻塞后面的代码,会先执行async外面的同步代码,等外面的同步代码执行完成在执行async中的代码。
如果它等到的是一个 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,
然后我们可以再想想为什么async1 then
会在promise3 end
后打印