前言

本文其实是对上一文的详解,上一篇只是把问题抛出来,由于题目的运行结果着实有些让人脑瓜疼,所以需要单独一文进行详解。希望能给读者一点启发。


故事要从一道今日头条的笔试题说起~ 

题目来源:半年工作经验今日头条和美团面试题面经分享!!!!!


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里面的事件循环和回调队列咯~
今天题主假设看客都已经了解了setTimeout是宏任务会在最后执行的前提(因为它不是今天要讨论的重点),我们主要来讲讲​promise​、​async​和​await​之间的关系。


先上正确答案:

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


事实上,没有在控制台执行打印之前,我觉得它应该是这样输出的:

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


为什么这样认为呢?因为我们(粗浅地)知道await之后的语句会等await表达式中的函数执行完得到结果后,才会继续执行。


MDN​是这样描述await的:

async 函数中可能会有 await 表达式,这会使 async 函数暂停执行,等待表达式中的 Promise 解析完成后继续执行 async 函数并返回解决结果。

会认为输出结果是以上的样子,是因为没有真正理解这句话的含义。

阮一峰老师的解释我觉得更容易理解:

对啦就是这样,MDN描述的暂停执行,实际上是​让出了线程(跳出async函数体)​然后继续执行后面的脚本的。这样一来我们就明白了,所以我们再看看上面那道题,按照这样描述那么他的输出结果就应该是:

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


好像哪里不太对?对比控制台输出的正确结果,咦~有两句输出是不一样的呀!!


async1 end
promise2


为什么会这样呢?这也是这道题目最难理解的一个地方。要搞明白这个事情,我们需要先来回顾一些概念:

async



async function 声明将定义一个返回 AsyncFunction 对象的异步函数。

当调用一个 async 函数时,会返回一个 Promise 对象。当这个 async 函数返回一个值时,Promise 的 resolve 方法会负责传递这个值;当 async 函数抛出异常时,Promise 的 reject 方法也会传递这个异常值。



所以你现在知道咯,使用 ​async​ 定义的函数,当它被调用时,它返回的其实是一个Promise对象。

我们再来看看 ​await​ 表达式执行会返回什么值。

await



语法:[return_value] = await expression;

表达式(express):一个 Promise 对象或者任何要等待的值。

返回值(return_value):返回 Promise 对象的​处理结果​。如果等待的不是 Promise 对象,则返回该值本身。




所以,当await操作符后面的表达式是一个Promise的时候,它的返回值,实际上就是Promise的回调函数resolve的参数。

明白了这两个事情后,我还要再啰嗦两句。我们都知道Promise是一个立即执行函数,但是他的成功(或失败:reject)的回调函数resolve却是一个异步执行的回调。​当执行到resolve()时,这个任务会被放入到回调队列中,等待调用栈有空闲时事件循环再来取走它。



终于进入正文:解题

好了铺垫完这些概念,我们回过头看上面那道题目困惑的那两句关键的地方(建议一边对着题目一边看解析我怕我讲的太快你跟不上啊哈哈????)。

执行到 async1 这个函数时,首先会打印出“async1 start”(这个不用多说了吧,async 表达式定义的函数也是立即执行的);

然后执行到 await async2(),发现 async2 也是个 async 定义的函数,所以直接执行了“console.log(‘async2’)”,同时async2返回了一个Promise,​划重点:此时返回的Promise会被放入到回调队列中等待,await会让出线程(js是单线程还用我介绍吗),接下来就会跳出 async1函数 继续往下执行。

然后执行到 new Promise,前面说过了promise是立即执行的,所以先打印出来“promise1”,然后执行到 resolve 的时候,resolve这个任务就被放到回调队列中(前面都讲过了上课要好好听啊喂)等待,然后跳出Promise继续往下执行,输出“script end”。

接下来是重头戏。同步的事件都循环执行完了,调用栈现在已经空出来了,那么事件循环就会去回调队列里面取任务继续放到调用栈里面了。

这时候取到的第一个任务,就是前面 async1 放进去的Promise,执行Promise时发现又遇到了他的真命天子resolve函数,​划重点:这个resolve又会被放入任务队列继续等待,然后再次跳出 async1函数 继续下一个任务。

接下来取到的下一个任务,就是前面 new Promise 放进去的 ​resolve回调​ 啦 yohoo~这个resolve被放到调用栈执行,并输出“promise2”,然后继续取下一个任务。

后面的事情相信你已经猜到了,没错调用栈再次空出来了,事件循环就取到了下一个任务:​历经千辛万苦终于轮到的那个Promise的resolve回调!!!​执行它(啥也不会打印的,因为 async2 并没有return东西,所以这个resolve的参数是undefined),此时 await 定义的这个 Promise 已经执行完并且返回了结果,所以可以继续往下执行 async1函数 后面的任务了,那就是“console.log(‘async1 end’)”。

谜之困惑的那两句执行结果(“promise2”、“async1 end”)就是这样来的~

来图再理解下

promise、async和await之执行顺序的那点事_调用栈


总结


这道题目考的,其实是以下几个点:


  1. 调用栈
  2. 事件循环
  3. 任务队列
  4. promise的回调函数执行
  5. async表达式的返回值
  6. await表达式的作用和返回值

理解了这些,自然就明白了为什么答案是这样(答出笔试题还要分析给面试官原因哈哈哈)~

我自己曾经自认为理解了时间循环了,竟然还有这么一个坑在等着我。前端水真的深 啊。




【点个赞或者分享下,我就干的更带劲儿】

promise、async和await之执行顺序的那点事_事件循环_02

 ​重度前端--助力深度学习


为web前端同行提供有价值、有深度的技术文章

官网:http://bigerfe.com【建设】

promise、async和await之执行顺序的那点事_事件循环_03

长按二维码关注我