Bootstrap

promise、async、await异步原理与执行顺序

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

result:

script start
async1 start
async
promise1
script end
async1 end
promise2
setTimeout

MDN 是这样描述 await 的:

我们(粗浅地)知道 await 之后的语句会等 await 表达式中的函数执行完得到结果后,才会继续执行。
async 函数中可能会有 await 表达式,这会使 async 函数暂停执行,等待表达式中的 Promise 解析完成后继续执行 async 函数并返回解决结果。

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

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

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

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 表达式的作用和返回值

async和await的特点:
async:
1、async函数实质返回也是promise
2、加在一个函数前,可将其实现promise效果,函数中的return直接对应resolve

await:
1、await函数返回值也是promise
2、不可用作顶行,需放在async函数中使用
3、await后可跟promise, await,也可跟普通函数(就和普通函数一样)
4、等待拿取后跟函数的值,并非阻塞线程,后续任务仍可异步执行
5、有了await将很少用到then, 因为await是等待执行的
// async和await和try catch和结合,可以实现异步的异常处理控制

async function f() {
    await Promise.reject(new Error('whoops!'))
}
// 和下面一样
async function f() {
    throw new Error('Whoops!')
}   

var getReturn = await Promise.resolve(1) // 这样就不再需要另外的then接收promise的返回值,await直接等待返回值

资料参考

关于 async 和 await 的执行顺序这里
https://segmentfault.com/a/1190000011296839

关于调用栈、事件循环、任务队列可以点这里
https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md

async和await详解
https://segmentfault.com/a/1190000013292562?utm_source=channel-newest

;