理解JavaScript的async/await

随着 Node 7 的发布,越来越多的人开始研究据说是异步编程终级解决方案的 async/await。我第一次看到这组关键字并不是在
JavaScript 语言里,而是在 c# 5.0 的语法中。C# 的 async/await 需要在 .NET Framework 4.5
以上的版本中使用,因此我还很悲伤了一阵——为了要兼容 XP 系统,我们开发的软件不能使用高于 4.0 版本的 .NET Framework。

我之前在《闲谈异步调用“扁平”化》 中就谈到了这个问题。无论是在 C# 还是 JavaScript 中,async/await
都是非常棒的特性,它们也都是非常甜的语法糖。C# 的 async/await 实现离不开 Task 或 Task<Result>
类,而 JavaScript 的 async/await 实现,也离不开Promise。

现在抛开 C# 和 .NET Framework,专心研究下 JavaScript 的 async/await。

async 和 await 在干什么

任意一个名称都是有意义的,先从字面意思来理解。async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

另外还有一个很有意思的语法规定,await 只能出现在 async 函数中。然后细心的朋友会产生一个疑问,如果 await 只能出现在 async 函数中,那这个 async 函数应该怎么调用?

如果需要通过 await 来调用一个 async 函数,那这个调用的外面必须得再包一个 async 函数,然后……进入死循环,永无出头之日……

如果 async 函数不需要 await 来调用,那 async 到底起个啥作用?

async 起什么作用

这个问题的关键在于,async 函数是怎么处理它的返回值的!

我们当然希望它能直接通过 return 语句返回我们想要的值,但是如果真是这样,似乎就没 await 什么事了。所以,写段代码来试试,看它到底会返回什么:


  1. async function testAsync() { 
  2.     return "hello async"; 
  3.  
  4. const result = testAsync(); 
  5. console.log(result);  

看到输出就恍然大悟了——输出的是一个 Promise 对象。


  1. c:\var\test> node --harmony_async_await . 
  2. Promise { 'hello async' }  

所以,async 函数返回的是一个 Promise 对象。从文档中也可以得到这个信息。async
函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async
会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样


  1. testAsync().then(v => { 
  2.     console.log(v);    // 输出 hello async 
  3. });  

现在回过头来想下,如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)。

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

那么下一个关键点就在于 await 关键字了。

await 到底在等啥

一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是
await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise
对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行


  1. function getSomething() { 
  2.     return "something"; 
  3.  
  4. async function testAsync() { 
  5.     return Promise.resolve("hello async"); 
  6.  
  7. async function test() { 
  8.     const v1 = await getSomething(); 
  9.     const v2 = await testAsync(); 
  10.     console.log(v1, v2); 
  11.  
  12. test();  

await 等到了要等的,然后呢

await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?我不得不先说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

看到上面的阻塞一词,心慌了吧……放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。

async/await 帮我们干了啥

作个简单的比较

上面已经说明了 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写


  1. function takeLongTime() { 
  2.     return new Promise(resolve => { 
  3.         setTimeout(() => resolve("long_time_value"), 1000); 
  4.     }); 
  5.  
  6. takeLongTime().then(v => { 
  7.     console.log("got", v); 
  8. });  

如果改用 async/await 呢,会是这样


  1. function takeLongTime() { 
  2.     return new Promise(resolve => { 
  3.         setTimeout(() => resolve("long_time_value"), 1000); 
  4.     }); 
  5.  
  6. async function test() { 
  7.     const v = await takeLongTime(); 
  8.     console.log(v); 
  9.  
  10. test();  

眼尖的同学已经发现 takeLongTime() 没有申明为 async。实际上,takeLongTime() 本身就是返回的 Promise 对象,加不加 async结果都一样,如果没明白,请回过头再去看看上面的“async 起什么作用”。

又一个疑问产生了,这两段代码,两种方式对异步调用的处理(实际就是对 Promise 对象的处理)差别并不明显,甚至使用 async/await 还需要多写一些代码,那它的优势到底在哪?

async/await 的优势在于处理 then 链

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then
链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await
来进一步优化它)。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:


  1. /** 
  2.  * 传入参数 n,表示这个函数执行的时间(毫秒) 
  3.  * 执行的结果是 n + 200,这个值将用于下一步骤 
  4.  */ 
  5. function takeLongTime(n) { 
  6.     return new Promise(resolve => { 
  7.         setTimeout(() => resolve(n + 200), n); 
  8.     }); 
  9.  
  10. function step1(n) { 
  11.     console.log(`step1 with ${n}`); 
  12.     return takeLongTime(n); 
  13.  
  14. function step2(n) { 
  15.     console.log(`step2 with ${n}`); 
  16.     return takeLongTime(n); 
  17.  
  18. function step3(n) { 
  19.     console.log(`step3 with ${n}`); 
  20.     return takeLongTime(n); 
  21. }  

现在用 Promise 方式来实现这三个步骤的处理


  1. function doIt() { 
  2.     console.time("doIt"); 
  3.     const time1 = 300; 
  4.     step1(time1) 
  5.         .then(time2 => step2(time2)) 
  6.         .then(time3 => step3(time3)) 
  7.         .then(result => { 
  8.             console.log(`result is ${result}`); 
  9.             console.timeEnd("doIt"); 
  10.         }); 
  11.  
  12. doIt(); 
  13.  
  14. // c:\var\test>node --harmony_async_await . 
  15. // step1 with 300 
  16. // step2 with 500 
  17. // step3 with 700 
  18. // result is 900 
  19. // doIt: 1507.251ms  

输出结果 result 是 step3() 的参数 700 + 200 = 900。doIt() 顺序执行了三个步骤,一共用了 300 +
500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

如果用 async/await 来实现呢,会是这样


  1. async function doIt() { 
  2.     console.time("doIt"); 
  3.     const time1 = 300; 
  4.     const time2 = await step1(time1); 
  5.     const time3 = await step2(time2); 
  6.     const result = await step3(time3); 
  7.     console.log(`result is ${result}`); 
  8.     console.timeEnd("doIt"); 
  9.  
  10. doIt();  

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样

还有更酷的

现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。


  1. function step1(n) { 
  2.     console.log(`step1 with ${n}`); 
  3.     return takeLongTime(n); 
  4.  
  5. function step2(m, n) { 
  6.     console.log(`step2 with ${m} and ${n}`); 
  7.     return takeLongTime(m + n); 
  8.  
  9. function step3(k, m, n) { 
  10.     console.log(`step3 with ${k}, ${m} and ${n}`); 
  11.     return takeLongTime(k + m + n); 
  12. }  

这回先用 async/await 来写:


  1. async function doIt() { 
  2.     console.time("doIt"); 
  3.     const time1 = 300; 
  4.     const time2 = await step1(time1); 
  5.     const time3 = await step2(time1, time2); 
  6.     const result = await step3(time1, time2, time3); 
  7.     console.log(`result is ${result}`); 
  8.     console.timeEnd("doIt"); 
  9.  
  10. doIt(); 
  11.  
  12. // c:\var\test>node --harmony_async_await . 
  13. // step1 with 300 
  14. // step2 with 800 = 300 + 500 
  15. // step3 with 1800 = 300 + 500 + 1000 
  16. // result is 2000 
  17. // doIt: 2907.387ms  

除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?


  1. function doIt() { 
  2.     console.time("doIt"); 
  3.     const time1 = 300; 
  4.     step1(time1) 
  5.         .then(time2 => { 
  6.             return step2(time1, time2) 
  7.                 .then(time3 => [time1, time2, time3]); 
  8.         }) 
  9.         .then(times => { 
  10.             const [time1, time2, time3] = times; 
  11.             return step3(time1, time2, time3); 
  12.         }) 
  13.         .then(result => { 
  14.             console.log(`result is ${result}`); 
  15.             console.timeEnd("doIt"); 
  16.         }); 
  17.  
  18. doIt();  

有没有感觉有点复杂的样子?那一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,看着就晕!

洗洗睡吧

就目前来说,已经理解 async/await 了吧?但其实还有一些事情没提及——Promise 有可能 reject 啊,怎么处理呢?如果需要并行处理3个步骤,再等待所有结果,又该怎么处理呢?

作者:边城

来源:51CTO

时间: 2024-10-12 10:04:35

理解JavaScript的async/await的相关文章

JavaScript 的 Async/Await 完胜 Promise 的六个理由

提醒一下各位,Node 现在从版本 7.6 开始就支持 async/await 了.如果你还没有试过它,这里有一堆带有示例的理由来说明为什么你应该马上采用它,并且再也不会回头. 貌似嵌入 gist 上的代码在 medium 原生 app 中不行,但是在移动浏览器上可以.如果你是在 app 中读本文,请点击共享图标,选择"在浏览器中打开",才看得到代码片段. Async/await 101 对于那些从未听说过这个话题的人来说,如下是一个简单的介绍: Async/await 是一种编写异步

JavaScript 异步方案 async/await 实例教程

构建一个应用程序总是会面对异步调用,不论是在 Web 前端界面,还是 Node.js 服务端都是如此, JavaScript 里面处理异步调用一直是非常恶心的一件事情.以前只能通过回调函数,后来渐渐又演化出来很多方案,最后 Promise 以简单.易用.兼容性好取胜,但是仍然有非常多的问题.其实 JavaScript 一直想在语言层面彻底解决这个问题,在 ES6 中就已经支持原生的 Promise ,还引入了 Generator 函数,终于在 ES7 中决定支持 async 和 await .

JavaScript中的await/async的作用和用法_javascript技巧

await/async 是 ES7 最重要特性之一,它是目前为止 JS 最佳的异步解决方案了.虽然没有在 ES2016 中录入,但很快就到来,目前已经在 ES-Next Stage 4 阶段. 直接上例子,比如我们需要按顺序获取:产品数据=>用户数据=>评论数据 老朋友 Ajax 传统的写法,无需解释 // 获取产品数据 ajax('products.json', (products) => { console.log('AJAX/products >>>', JSON

深入理解JavaScript编程中的同步与异步机制

  这篇文章主要介绍了深入理解JavaScript编程中的同步与异步机制,不仅仅是AJAX已经深入到了各个角落,Node.js的火爆也让JS的异步编程格外引人注目,需要的朋友可以参考下 JavaScript的优势之一是其如何处理异步代码.异步代码会被放入一个事件队列,等到所有其他代码执行后才进行,而不会阻塞线程.然而,对于初学者来说,书写异步代码可能会比较困难.而在这篇文章里,我将会消除你可能会有的任何困惑. 理解异步代码 JavaScript最基础的异步函数是setTimeout和setInt

如何在现有代码中通过async/await实现并行

一项新技术或者一个新特性,只有你用它解决实际问题后,才能真正体会到它的魅力,真正理解它.也期待大家能够多分享解一些解决实际问题的内容. 在我们遭遇"黑色30秒"问题的过程中,切身体会到了异步的巨大作用(详见从ASP.NET线程角度对"黑色30秒"问题的全新分析),于是开始逐步地用async/await改造现有代码. 今天早上在将一个MVC Controller中的Action改为异步的时候突然发现--其中有7个方法调用可以并行执行. public async Tas

Async/Await替代Promise的6个理由

译者按: Node.js的异步编程方式有效提高了应用性能;然而回调地狱却让人望而生畏,Promise让我们告别回调函数,写出更优雅的异步代码;在实践过程中,却发现Promise并不完美;技术进步是无止境的,这时,我们有了Async/Await. Node.js 7.6已经支持async/await了,如果你还没有试过,这篇博客将告诉你为什么要用它. Async/Await简介 对于从未听说过async/await的朋友,下面是简介: async/await是写异步代码的新方式,以前的方法有回调函

C# async/await 使用总结

今天搞这两个关键字搞得有点晕,主要还是没有彻底理解其中的原理.   混淆了一个调用异步方法的概念: 在调用异步方法时,虽然方法返回一个 Task,但是其中的代码已经开始执行.该方法在调用时,即刻执行了一部分代码,直接最底层的 Async API 处才产生真正的异步操作,这时向上逐步返回,并最终使用一个 Task 来代表该异步任务. 当不使用 await 关键字时,该异步方法同样在异步执行.而使用 await 关键字后,只不过是对 Task(awaitable) 对象异步等待其执行结束,然后再同上

如何理解 JavaScript 中的 Promise 机制

本文讲的是如何理解 JavaScript 中的 Promise 机制, Promise 的世界 原生 Promises 是在 ES2015 对 JavaScript 做出最大的改变.它的出现消除了采用 callback 机制的很多潜在问题,并允许我们采用近乎同步的逻辑去写异步代码. 可以说 promises 和 generators ,代表了异步编程的新标准.不论你是否用它,你都得 必须 明白它们究竟是什么. Promise 提供了相当简单的 API ,但也增加了一点学习曲线.如果你以前从没见过

async &amp; await 的前世今生

原文:async & await 的前世今生   async 和 await 出现在C# 5.0之后,给并行编程带来了不少的方便,特别是当在MVC中的Action也变成async之后,有点开始什么都是async的味道了.但是这也给我们编程埋下了一些隐患,有时候可能会产生一些我们自己都不知道怎么产生的Bug,特别是如果连线程基础没有理解的情况下,更不知道如何去处理了.那今天我们就来好好看看这两兄弟和他们的叔叔(Task)爷爷(Thread)们到底有什么区别和特点,本文将会对Thread 到 Tas