promise、async和await之执行顺序的那点事

43
原文地址:https://lvdingjin.github.io/tech/2018/05/27/async-and-await.html

故事要从一道今日头条的笔试题说起~
题目来源:半年工作经验今日头条和美团面试题面经分享!!!!!

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是宏任务会在最后执行的前提(因为它不是今天要讨论的重点),我们主要来讲讲promiseasyncawait之间的关系。

先上正确答案:

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 函数并返回解决结果。

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

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

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

对啦就是这样,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”)就是这样来的~

总结

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

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

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

关于调用栈、事件循环、任务队列可以点这里了解更详细的描述。

为了方便大家直接贴图:
tupian

关于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

你可能感兴趣的

25 条评论
不要放弃 · 5月28日

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout 楼主,这个是我刚刚在浏览器执行的顺序

回复

0

@不要放弃 你的浏览器版本是什么?

lvdingjin 作者 · 5月28日
0

如果只能运行在特定浏览器,那么这篇文章的参考价值就大打折扣了,不过我确实学到了,谢谢楼主。用的是chromium 版本55.0.2883.84

不要放弃 · 5月28日
0

@不要放弃 并非只能运行在特定浏览器,而是不同版本的Chrome的V8表现会不一样,async和await这种比较新的语法在新旧V8引擎环境中表现自然会不一样~我用的是chrome最新66版本,可能的确存在差异,至于新旧版本差别的具体原因待我进一步了解~感谢告知和建议~

lvdingjin 作者 · 5月28日
yeachen · 5月28日

问个问题哈。既然 async 中当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句,为什么 async1 中 await async2()没有跳出来,而是直接执行了 async2

回复

0

@yeachen 对于阮一峰老师的原话“一旦遇到 await 就会先返回”这句话的理解,实际上不是说“遇到”就返回的,mdn的官方定义有描述await这个操作符的具体表现,假如await后面的表达式(本题中就是async2这个函数)不是一个promise,那么会直接执行不需要让出线程,如果返回的是promise则需要等promise解析完成后再继续执行,所以这里await操作符是会去解析执行async2函数的,而promise又是立即执行函数所以会输出“async2”。

lvdingjin 作者 · 5月28日
0

没有抛错所以是resolve把

不要放弃 · 5月28日
0

@lvdingjin

如果await 后面的表达式不是函数,是不是也会让出线程呢?比如下面的这个例子 await 123 也让让出了线程呢?

async function async1() {
            await 123
            console.log( 'end' )
        }
        async1()
        console.log( '同步代码' )
ziwei3749 · 12月2日
shenshuaijia · 5月28日

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

node v8.9.1

回复

0

引用贺师俊老师的一段解释:“一般来说,当遇到Chrome和Node.js在JavaScript运行方面有差异,应以最新的Chrome的行为为准。虽然Chrome和Node.js都使用V8为其JavaScript引擎,但两者的V8更新策略不同。Chrome每次升级会同时更新到V8的最新版。而Node更新小版本时V8也只更新小版本,只有Node更新大版本时才会更新V8大版本。所以绝大部分时候Node的V8会比同时期的Chrome的V8要落后。然后,比较老的V8(即目前Node使用的V8)对Promise和Async/Await的处理可能有少许不完全符合标准之处。”题主分别在node的8.11.2和最新版的10.2.1上运行这段代码后,前者得到的是:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
后者得到的是:
script start
async1 start
async2
promise1
script end
promise2
async1 end
setTimeout
这恰恰印证了贺师俊老师所描述的新老版本V8对Promise和Async/Await的处理上存在的差异。
附上贺老师原回答链接:https://www.zhihu.com/questio...

lvdingjin 作者 · 5月28日
yuanxin · 5月28日

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

node v8.9.4

回复

0

详情请移步上一条评论的回复

lvdingjin 作者 · 5月28日
0

多谢

yuanxin · 5月29日
UKer · 5月29日

移除确定的部分,将async用promise重写,很自然的就明白了

new Promise((resolve, reject) => {
  console.log("async1 start");
  resolve();
})
.then(() => {
    console.log("async2");
})
.then(() => {
    console.log("async1 end");
});

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

回复

1

上面的第二个then错误,修改一下

new Promise((resolve, reject) => {
  console.log("async1 start");
  console.log("async2");
  resolve(Promise.resolve());
}).then(() => {
  console.log("async1 end");
});

new Promise(function(resolve) {
  console.log("promise1");
  resolve();
}).then(function() {
  console.log("promise2");
});
UKer · 5月29日
0

厉害,这一下子就明白

qzuser_5789078ac4a8f · 11月5日
0

改了一下这个例子:
new Promise((resolve, reject) => {
console.log("async1 start");
console.log("async2");
resolve(Promise.resolve());
}).then(() => {
console.log("async1 end");
});

new Promise(function(resolve) {
console.log("promise1");
resolve();
}).then(function() {
console.log("promise2");
})..then(function() {
console.log("promise3");
})..then(function() {
console.log("promise4");
});

最后执行的结果:
async1 start
async2
promise1
promise2
promise3
async1 end
promise4

原因说下:resolve(Promise.resolve())这里,应该是执行了Promise.resolve().then(),
后面如果跟的.then()应该是Promise.resolve().then().then(() => {
console.log("async1 end");
})
如此就可以上面的结果:promise2 —> Promise.resolve().then() -> promise3 -> async1 end

qzuser_5789078ac4a8f · 11月5日
dongfang · 8月7日

整体思路是ok的,。最关键那两个async1 end
promise2 先后和版本,环境都有关系。我觉着这块可不必纠结

回复

Dawn · 11月3日

楼主觉得阮一峰的这句话“函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句"准确吗?下面这段代码怎么解释呢?

async function a(){
await setTimeout(()=> console.log(3),5000)
console.log(1)
}  
a()

回复

0

await 后面相当于
Promise.resolve(setTimeout(()=> console.log(3),5000));
或者
await new Promise((resolve, reject) => {
resolve()
}).then(() => {
setTimeout(() => console.log(3), 5000)
})
执行时放入job queue;
等主队列空闲时再执行回调。

FuYinghao · 11月20日
ClexeR · 12月3日

你好,我有一个疑问

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' )
} ).then(()=>{console.log( 'promise3' )})
   .then(()=>{console.log( 'promise4' )})
   .then(()=>{console.log('promise5')})

console.log( 'script end' );

上面的执行结果是:
script start
async1 start
async2
promise1
script end
promise2
promise3
async1 end
promise4
promise5
setTimeout
不太明白产生这个结果的原因。。。

回复

specialCoder · 6 天前

感觉有个地方解释的不正确
对于await async2() ,从右向左执行
1.先执行async2() ,因为async2是async函数,所以会返回 Promise.resolve(undefined),此时并没有将这个promise放到微观任务队列
2.当遇到await的时候,让出线程,执行下面的同步代码

何时将async2返回的值放到微观队列的呢?
1.同步任务执行完之后回到await ,此时await等待的是个异步结果,等待异步返回之后才执行后面的代码,此时才将异步放到微观队列
2.然后从微观队列取promise2执行
3.然后从微观队列取Promise.resolve(undefined)执行await后面的代码

回复

载入中...