一道面试题:还在纠结async/ await、Promise的执行顺序?

1,887 阅读3分钟

await 关键字与异步函数一起使用时,它的真正优势就变得明显了 —— 事实上, await 只在异步函数里面才起作用。它可以放在任何异步的,基于 promise 的函数之前。它会暂停代码在该行上,直到 promise 完成,然后返回结果值。在暂停的同时,其他正在等待执行的代码就有机会执行了。 -- 摘自MDN

题目

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

console.log('illegalscript start');

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

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

console.log('illegalscript end');

题目的本质,就是考察setTimeout、promise、async await的实现及执行顺序,以及JS的事件循环的相关问题。我们知道,这无疑是涉及到的微任务与宏任务的执行顺序,我们不妨设两个队列: 微任务队列microQue、宏任务队列:macroQue来存放对应输出的值

题解过程

首先, 两队列为空:
microQue:[]
macroQue: []

代码从上而下执行依次:

  1. 首先定义两个异步函数 async1async2

  2. 执行

    console.log('illegalscript start'); 
    

    首先输出 :"illegalscript start"

    当前状态

    已输出:"illegalscript start"
    微任务队列: []
    宏任务队列: []

  3. 执行:

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

    setTimeout推进一个宏任务输出:console.log('setTimeout');

    当前状态:

    已输出:"illegalscript start"
    微任务队列: []
    宏任务队列: ["console.log('setTimeout')"]

  4. 执行

    async1()
    

立即输出:"async1 start", 随后执行 await async2()
这里是整个题目最容易出错的地方,要认真审题: 回顾文章开头的引用:

await 只在异步函数里面才起作用。它可以放在任何异步的,基于 promise 的函数之前。它会暂停代码在该行上,直到 promise 完成,然后返回结果值。在暂停的同时,其他正在等待执行的代码就有机会执行了

await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。 说那么多怕是要晕了,其实很简单,看例子就明白:

async function async1() {
 console.log('async1 start');
 await async2();
 console.log('async1 end');
}
等价于
async function async1() {
 console.log('async1 start');
 Promise.resolve(async2()).then(() => {
                console.log('async1 end');
        })
}

好,看回题目,此执行阶段,立即输出:"async2"
执行至此,async1函数里面剩余代码暂停执行直到 promise 完成,即剩余代码添加至微任务队列中去。此时正在嗷嗷待哺的正在等待执行的代码有了机会,继续往下执行。

当前状态:

已输出:"illegalscript start", "async1 start", "async2"
微任务队列: ["console.log('async1 end')"]
宏任务队列: ["console.log('setTimeout')"]

  1. 执行:

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

立即输出:promise1
执行resolve(),then里面内容推进微任务队列

当前状态:

已输出:"illegalscript start", "async1 start", "async2" "promise1"
微任务队列: ["console.log('async1 end')", "console.log('promise2')"]
宏任务队列: ["console.log('setTimeout')"]

  1. 执行:

    console.log('illegalscript end');
    

    立即输出:"illegalscript end"

    已输出:"illegalscript start", "async1 start", "async2" "promise1" "illegalscript end"
    微任务队列: ["console.log('async1 end')", "console.log('promise2')"]
    宏任务队列: ["console.log('setTimeout')"]

  2. 本轮事件循环已结束,开始执行并清空微任务队列:
    依次输出:"async1 end""promise2"

    当前状态

    已输出:"illegalscript start", "async1 start", "async2" "promise1" "illegalscript end" "async1 end" "promise2"
    微任务队列: ["console.log('async1 end')", "console.log('promise2')"]
    宏任务队列: ["console.log('setTimeout')"]

  3. 进入下一事件循环,执行清空宏任务队列 输出: setTimeout

    当前状态:

    已输出:"illegalscript start", "async1 start", "async2" "promise1" "illegalscript end" "async1 end" "promise2" "setTimeout"
    微任务队列: []
    宏任务队列: []

至此,代码已执行完毕,微任务队列以及宏任务队列也已清空,执行结束。 所以最终输出结果先后顺序为:

  • illegalscript start
  • async1 start
  • async2
  • promise1
  • illegalscript end
  • async1 end
  • promise2
  • setTimeout

如图:

本文使用 mdnice 排版