async/await 和 promise 的执行顺序

4,899 阅读5分钟

话不多说,先上题,据说是前两年一道烂大街的头条面试题

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');

那么打印顺序是什么呢? 这其中涉及到了js中的eventloop/async/await/promise,可以先尝试作答一下 下面公布答案

script start
async1 start
async2
promise1
script end
async1 end
promise2
undefined
setTimeout

上述执行结果是在chrome 96.0 上运行的结果,可能与前几年的运行结果不一致了,主要是chrome版本的问题

下面来详细讲解一下为什么是这个顺序

一句话解释:await会阻塞后面的任务,指的是下一行代码,await同行代码是会立即执行的

首先需要明确一个概念,其实async await实质只是promise.then 的语法糖,带 async 关键字的函数,它使得你的函数的返回值必定是 promise 对象,如果async关键字函数返回的不是promise,会自动用Promise.resolve()包装,如果async关键字函数显式地返回promise,那就以你返回的promise为准。

对于await来说,如果await后面是不是promise对象,那么await会阻塞后面的代码,先执行async函数外面的同步代码,同步代码执行完毕,再回到async内部,把这个非promise的东西,作为await表达式的结果。如果await后面是promise对象,那么他会在async外部的同步代码执行完毕之后等到promise对象fulfilled,然后把resolve的参数作为await表达式的运行结果。 其次放一个宏任务微任务的图便于理解

image.png

在每一层(一次)的事件循环中,首先整体代码块看作一个宏任务,宏任务中的 Promise(then、catch、finally)、MutationObserver、Process.nextTick就是该宏任务层的微任务,宏任务中的同步代码进入主线程中立即执行的,宏任务中的非微任务的异步代码将作为下一次循环时的宏任务进入的调用栈等待执行,此时,调用栈中的等待执行队列分为两种,分别是优先级较高的本层循环中的微任务队列,以及优先级低的下次循环执行的宏任务队列。

每一个宏任务队列都可以理解为当前的主线程,js总是先执行主线程上的任务,执行完毕后执行当前宏任务队列上的所有微任务,先进先出原则,在执行完这一个宏任务队列上的所有微任务之后,才会继续执行下一个宏任务。

  1. 不管是同步还是异步,js都会按顺序执行,只是不等待异步的执行结果而已
  2. 同步的任务没有优先级之分,异步执行有优先级,先执行微任务(microtask队列),再执行宏任务(macrotask队列),同级别按顺序执行   微任务: process.nextTickpromiseMutationObserver

  宏任务:scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

首先js是单线程的,所以先执行主线程上的任务

  1. 也就是console.log('script start')最先被执行输出了script start
  2. 接下来遇到setTimeout(),放入到下一个宏任务队列中,等待当前宏任务队列以及其微任务队列执行完毕再执行
  3. 然后执行async1()函数,这个时候实质上是创建了一个promise对象,而promise的构造函数的运行是在主任务队列中的,所以会立即执行async1 start,然后执行async2()函数,并返回一个async2.then(()=>{console.log('async1 end');}),这里就会把.then()里面的内容放到微任务队列中,我们将其命名为task1,等待主线程执行完毕后执行,同时也会执行async2()的构造函数,输出async2
  4. 然后执行了new Promise(),这里会直接输出构造函数内部的内容,所以输出了promise1
  5. 然后执行resolve()函数,那么会进入到then()中,promise.then(console.log('promise2');)是一个异步任务,会被放入到微任务队列中,我们将其命名为task2。
  6. 然后执行最后的主线程任务 console.log('script end');输出 script end
  7. 此时主线程上的同步任务执行完毕,开始执行当前主线程下的微任务,即task1,task2,依次输出async1 endpromise2。微任务队列执行完毕
  8. 然后开始执行下一个宏任务队列,即setTimeout(),输出setTimeout

然后再拓展一下,在存在多个await的时候,它和promise的执行顺序又是什么样的呢?

async function async1() {
    console.log('async1 start');
    await async2();                
    console.log('async1 end');
}
async function async2() {
    await async3(); 
    console.log('async2');
}
async function async3() {
    console.log('async3');
}
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');

chrome中结果如下:

script start
async1 start
async3
promise1
script end
async2
promise2
async1 end
undefined
setTimeout

这里对于

async2
promise2
async1 end

这里的顺序可能会有不明白的地方,我按照自己的理解解释一下,有不对的地方希望大佬指正一下:

首先前面一部分的内容都差不多,但是在上述第3步的时候,执行async1();函数的时候,先是执行了console.log('async1 start');输出了async1 start,然后await async2();首先执行async2()函数的内容,然后返回了async2().then(()=>{console.log('async1 end');}),然后执行async2()函数的内容的时候,遇到了await async3();,这里需要先执行async3(),执行console.log('async3');,输出async3,然后返回async3().then(()=>{console.log('async2')}),有因为await async3()其实是在async2()函数中的,所以这里可以抽象成async3().then(()=>{console.log('async2')}).then(()=>{console.log('async1 end');}),这里命名为task1,然后中间的过程一样,promise.then(console.log('promise2');),依旧为task2,到了执行微任务的时候,首先执行async3().then(()=>{console.log('async2')})的时候,输出了async2,然后这个时候到了.then(),他会再次生成一个微任务添加到微任务队列中,也就产生了task3,其实是.then(()=>{console.log('async1 end');}),然后执行task2,输出promise2,最后再执行task3,输出了async1 end,其他就和上面的基本一致了。