深入理解JavaScript的async/await
async和await在干什么
任意一个名称都是有意义的,先从字面意思来理解。async是“异步”的简写,而await的意思是等待。所以应该很好理解async用于申明一个function是异步的,而await等待某个操作完成。
那么async/await到底是干嘛的呢?我们先来简单介绍一下。
- async/await是一种编写异步代码的新方法。之前异步代码的方案是回调和promise。
- async/await是建立在promise的基础上。(对promise不熟悉的同学可以看一下这篇文章入门Promise的正确姿势)
- async/await像promise一样,也是非阻塞的。
- async/await让异步代码看起来、表现起来更像同步代码。这正是其威力所在。
async起什么作用
这个问题的关键在于,async函数是怎么处理它的返回值的!
我们当然希望它能直接通过return语句返回我们想要的值,但是如果真是这样,似乎就没await什么事了。所以,写段代码来试试,看它到底会返回什么:
看到输出就恍然大悟了——输出的是一个Promise对象。
Promise{
:"helloasync"}
所以,async函数返回的是一个Promise对象。async函数(包含函数语句、函数表达式、Lambda表达式)会返回一个Promise对象,如果在函数中return一个直接量,async会把这个直接量通过Promise.resolve()封装成Promise对象。
async函数返回的是一个Promise对象,所以在最外层不能用await获取其返回值的情况下,我们当然应该用原来的方式:then()链来处理这个Promise对象,就像这样
asyncfunctiontest(){ return'helloasync'; } test().then((val)=>{ console.log(val);//helloasync })
现在回过头来想下,如果async函数没有返回值,又该如何?很容易想到,它会返回Promise.resolve(undefined)。
联想一下Promise的特点——无等待,所以在没有await的情况下执行async函数,它会立即执行,返回一个Promise对象,并且,绝不会阻塞后面的语句。这和普通返回Promise对象的函数并无二致。
那么下一个关键点就在于await关键字了。
await到底在等啥
一般来说,都认为await是在等待一个async函数完成。不过按语法说明,await等待的是一个表达式,这个表达式的计算结果是Promise对象或者其它值(换句话说,就是没有特殊限定)。
因为async函数返回一个Promise对象,所以await可以用于等待一个async函数的返回值——这也可以说是await在等async函数,但要清楚,它等的实际是一个返回值。注意到await不仅仅用于等Promise对象,它可以等任意表达式的结果,所以,await后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行。
functiongetSomething(){ return"something"; } asyncfunctiontestAsync(){ returnPromise.resolve('helloasync'); } asyncfunctiontest(){ letv1=awaitgetSomething(); letv2=awaittestAsync(); console.log(v1,v2); } test(); console.log('我执行了'); //执行结果为: //我执行了 //something,helloasync
await等到了要等的,然后呢
await等到了它要等的东西,一个Promise对象,或者其它值,然后呢?我不得不先说,await是个运算符,用于组成表达式,await表达式的运算结果取决于它等的东西。
如果它等到的不是一个Promise对象,那await表达式的运算结果就是它等到的东西。
如果它等到的是一个Promise对象,await就忙起来了,它会阻塞后面的代码,等着Promise对象resolve,然后得到resolve的值,作为await表达式的运算结果。
看到上面的阻塞一词,心慌了吧……放心,这就是await必须用在async函数中的原因。async函数调用不会造成阻塞(也就是第13行代码不会被阻塞),它内部所有的阻塞都被封装在一个Promise对象中异步执行。
async/await帮我们干了啥
作个简单的比较
上面已经说明了async会将其后的函数(函数表达式或Lambda)的返回值封装成一个Promise对象,而await会等待这个Promise完成,并将其resolve的结果返回出来。
现在举例,用setTimeout模拟耗时的异步操作,先来看看不用async/await会怎么写。
functiontakeLongTime(){ returnnewPromise((resolve)=>{ setTimeout(()=>resolve('longtimevalue'),1000); }) } takeLongTime().then((v)=>{ console.log('get:',v); })
如果改用async/await呢,会是这样。
functiontakeLongTime(){ returnnewPromise((resolve)=>{ setTimeout(()=>resolve('longtimevalue'),1000); }) } asyncfunctiontest(){ letv=awaittakeLongTime();//等待异步操作的结果,阻塞后面代码的执行 console.log(v); }
眼尖的同学已经发现takeLongTime()没有申明为async。实际上,takeLongTime()本身就是返回的Promise对象,加不加async结果都一样,如果没明白,请回过头再去看看上面的“async起什么作用”。
又一个疑问产生了,这两段代码,两种方式对异步调用的处理(实际就是对Promise对象的处理)差别并不明显,甚至使用async/await还需要多写一些代码,那它的优势到底在哪?
async/await的优势在于处理then链
单一的Promise链并不能发现async/await的优势,但是,如果需要处理由多个Promise组成的then链的时候,优势就能体现出来了(很有意思,Promise通过then链来解决多层回调的问题,现在又用async/await来进一步优化它)。
假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用setTimeout来模拟异步操作:
/* *传入参数n,表示这个函数执行的时间(毫秒) *执行的结果是n+200,这个值将用于下一步骤 */ functiontakeLongTime(n){ returnnewPromise((resolve)=>{ setTimeout(()=>resolve(n+200),n); }) } functionstep1(n){ console.log(`step1with${n}`); returntakeLongTime(n); } functionstep2(n){ console.log(`step2with${n}`); returntakeLongTime(n); } functionstep3(n){ console.log(`step3with${n}`); returntakeLongTime(n); }
现在用Promise方式来实现这三个步骤的处理。
functiondoIt(){ console.time('doIt'); lettime1=300; step1(time1) .then((time2)=>step2(time2)) .then((time3)=>step3(time3)) .then((result)=>{ console.log(`resultis${result}`); console.timeEnd("doIt"); }) } doIt(); //执行结果为: //step1with300 //step2with500 //step3with700 //resultis900 //doIt:1510.2490234375ms
输出结果result是step3()的参数700+200=900。doIt()顺序执行了三个步骤,一共用了300+500+700=1500毫秒,和console.time()/console.timeEnd()计算的结果一致。
如果用async/await来实现呢,会是这样。
asyncfunctiondoIt(){ console.time('doIt'); lettime1=300; lettime2=awaitstep1(time1);//将Promise对象resolve(n+200)的值赋给time2 lettime3=awaitstep1(time2); letresult=awaitstep1(time3); console.log(`resultis${result}`); console.timeEnd('doIt'); } doIt(); //执行结果为: //step1with300 //step2with500 //step3with700 //resultis900 //doIt:1512.904296875ms
结果和之前的Promise实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样。
还有更酷的
现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。
/* *传入参数n,表示这个函数执行的时间(毫秒) *执行的结果是n+200,这个值将用于下一步骤 */ functiontakeLongTime(n){ returnnewPromise((resolve)=>{ setTimeout(()=>resolve(n+200),n); }) } functionstep1(n){ console.log(`step1with${n}`); returntakeLongTime(n); } functionstep2(m,n){ console.log(`step2with${m}+${n}`); returntakeLongTime(m+n); } functionstep3(k,m,n){ console.log(`step3with${k}+${m}+${n}`); returntakeLongTime(k+m+n); }
这回先用async/await来写:
asyncfunctiondoIt(){ console.time('doIt'); lettime1=300; lettime2=awaitstep1(time1);//将Promise对象resolve(n+200)的值赋给time2 lettime3=awaitstep2(time2,time1); letresult=awaitstep3(time3,time2,time1); console.log(`resultis${result}`); console.timeEnd('doIt'); } doIt(); //执行结果为: //step1with300 //step2with500+300 //step3with1000+500+300 //resultis2000 //doIt:2916.655029296875ms
除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成Promise方式实现会是什么样子?
functiondoIt(){ console.time('doIt'); lettime1=300; step1(time1) .then((time2)=>{ returnstep2(time1,time2) .then((time3)=>[time1,time2,time3])//step3需要用到time1,time2,time3,因此需要返回 }) .then((times)=>{ let[time1,time2,time3]=times; returnstep3(time1,time2,time3) }) .then((result)=>{ console.log(`resultis${result}`); console.timeEnd('doIt'); }) } doIt(); //执行结果为: //step1with300 //step2with300+500 //step3with300+500+1000 //resultis2000 //doIt:2919.49609375ms
有没有感觉有点复杂的样子?那一堆参数处理,就是Promise方案的死穴——参数传递太麻烦了,看着就晕!
注意点
就目前来说,已经理解async/await了吧?但其实还有一些事情没提及——Promise有可能reject啊,怎么处理呢?
await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。
asyncfunctionmyFunction(){ try{ awaitsomethingThatReturnAPromise(); }catch(err){ console.log(err); } } //另一种写法 asyncfunctionmyFunction(){ awaitsomethingThatReturnAPromise().catch(function(err){ console.log(err); }) }