从面试题入手,如何理解JavaScript中的async/await和promise的执行顺序

前言:关于js的异步执行顺序,宏任务、微任务这些,或者async/await这些概念,相信大家都已经耳熟能详了。现在,我们从一道前端面试题入手,检查一下自己对这些概念理解的程度,用于自查。

一、下面,我们来看看这道面试题:

其实,这道题的关键,不仅是说出正确的打印顺序,更重要的是能否说清楚每一个步骤,为什么这样执行。(面试的时候,有时候,面试官会想知道你的解题思路的。)

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

以下答案,以游览器的eventloop机制为准的:

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

如果回答出来了正确的顺序,恭喜你已经完全理解了这些概念;如果回答有些误差,也没有关系,可以和我一起再来复习一遍吧。 

二、前置内容

       第1部分:对于async await的理解

       主要会讲解3点内容:

  • async做一件什么事情?
  • await在等什么?
  • await等到之后,做了一件什么事情?
  • async/await比pormise有哪些优势?

       async做了一件什么事情?

       带async关键字的函数,它使得你的函数的返回值必定是promise对象。

       如果async关键字函数返回的不是promise,会自动用promise.resolve()包装;

       如果async关键字函数显式地返回promise,那就以你返回的promise为准。

       那我们从一个简单的例子,来看下async关键字函数和普通函数返回值的区别吧:

async function fn1(){
    return 123
}

function fn2(){
    return 123
}

console.log(fn1())
console.log(fn2())
Promise{<resolved>:123}
123

  可以看出来,async函数也没啥,它无非就是把return值包装了一下,其他就跟普通函数一样。

  关于async关键字还有哪些要注意的?

  • 在语义上要理解,async表示函数内部有异步操作;
  • 另外注意,一般await关键字要在async关键字函数的内部,await写在外面会报错。

    await在等什么?

    await等的是右侧【表达式】的结果

    右侧如果是函数,那么函数的return值就是【表达式】

    右侧如果是一个'hello'或者什么值,那表达式的结果就是'hello'

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

   可能大家都知道await会让出线程,阻塞后面的代码,那么上面的例子中,'async2'和'script start'谁先打印呢?

   是从左向右执行,一旦碰到await直接跳出,阻塞async2()的执行?

   还是从右向左,先执行async2后,发现有await关键字,于是让出线程,阻塞代码呢?

   实践的结论是,从右向左的。先打印async2,后打印的script start

   (之所以一提,因为经常看到这样的说法,【一旦遇到await就立刻让出线程,阻塞后面的代码】。这样的说法,会让我误以为,await后面的那个函数,async2()也直接被阻塞呢。)

   await等到之后,做了一件什么事情?

   那么右侧表达式的结果,就是await要等的东西。

  等到之后,对于await来说,分2个情况

  • 不是promise对象
  • 是promise对象

    如果不是promise,await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为await表达式的结果

    如果它等到的是一个promise对象,await也会暂停async后面的代码,先执行async外面的同步代码,等着promise对象fulfilled,然后把resolve的参数作为await表达式的运算结果。

三、画图来看清宏任务、微任务的执行过程,来解答开篇的面试题

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

   先分享一个个人理解的宏任务和微任务的概念,在脑海中的宏任务和微任务如图所示:

  也就是【宏任务】、【微任务】都是队列。

一段代码执行时,会先执行宏任务中的同步代码,

  • 如果执行中遇到setTimeout之类的宏任务,那么就把这个setTimeout内部的函数推入【宏任务的队列】中,下一轮宏任务执行时调用;
  • 如果执行中遇到promise.then()之类的微任务,就会推入到【当前宏任务的微任务队列】中,在本轮宏任务的同步代码执行都完成后,依次执行所有的微任务1、2、3

    下面就以面试题为例子,分析这段代码的执行顺序。

    每次宏任务和微任务发生变化时,画一个图来表示他们的变化。

    直接打印同步代码console.log('script start')

//首先是2个函数声明,虽然有async关键字,但不是调用就不看,然后首先是打印同步代码
console.log('script start')

   

   将setTimeout放入宏任务队列

   默认所包裹的代码,其实可以理解为是第一个宏任务,所以这里是宏任务2

   调用async1,打印同步代码console.log('async1 start')

   看到带有async关键字的函数,它仅仅是把return值包装成了promise,其他并没有什么不同的地方。所以就很普通的打印console.log('async1 start')

  分析一下await async2() 

  前文提过await,1.它先计算出右侧的结果;2.然后看到await后,中断async函数

  • 先得到await右侧表达式的结果,执行async2(),打印同步代码console.log('async2'),并且return Promise.resolve(undefined);
  • await后,中断async函数,先执行async外的同步代码。

    目前,就直接打印console.log('async2')

  被阻塞后,要执行async之外的代码

  执行new Promise(),Promise构造函数是直接调用的同步代码,所以console.log('promise1')

   代码运行到promise.then()

   代码运行到promise.then(),发现这个是微任务,所以暂时不打印,只是推入当前宏任务的微任务队列中。

   注意:这里只是把promise2/推入微任务队列,并没有执行。微任务会在当前宏任务的同步代码执行完毕,才会依次执行

  

  打印同步代码console.log('script end') 

  没什么好说的。执行完这个同步代码后,【async外的代码】终于走了一遍

  下面该回到await表达式那里,执行await Promise.resolve(undefined)了

   回到async内部,执行await Promise.resolve(undefined)

   对于await Promise.resolve(undefined)如何理解呢?

  如果一个promise被传递给一个await操作符,await将等待Promise正常处理完成并返回其处理结果。

  在这个例子中,就是Promise.resolve(undefined)正常处理完成,并返回其处理结果。那么await async2()就算是执行结束了。

  目前这个promised状态是fulfilled,等其处理结果返回就可以执行await下面的代码了。

   那何时能拿到处理结果呢?

  平时我们用promise,调用resolve后,何时能拿到处理结果?是不是需要在then的第一个参数里,才能拿到结果。

  所以,这里的await Promise.resolve()就类似于

Promise.resolve(undefined).then((undefined)=>{
    
})

   把then的第一个回调参数(undefined)=>{}推入微任务队列。

   then执行完,才是await async2()执行结束。

   await async2()执行结束,才能继续执行后面的代码

   如图

  此时,当前宏任务1都执行完了,要处理微任务队列里的代码。

  微任务队列,先进先出的原则:

  • 执行微任务1,打印promise2;
  • 执行微任务2,没什么内容

   但是,微任务2执行后,await async2()语句结束,后面的代码不再被阻塞,所以打印

  console.log('async1 end')

  宏任务1执行完成后,执行宏任务2

  宏任务2的执行比较简单,就是打印

  console.log('setTimeout')。

 参考博客:

  8张图帮你一步步看清 async/await 和 promise 的执行顺序   https://segmentfault.com/a/1190000017224799

  理解 JavaScript 的 async/await   https://segmentfault.com/a/1190000007535316

 【编程】充分理解JS async/await Promise 及 运行顺序  https://www.bilibili.com/read/cv4058649/

  

       

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值