用Async函数简化异步代码

Promise 在 JavaScript 上发布之初就在互联网上流行了起来 — 它们帮开发人员摆脱了回调地狱,解决了在很多地方困扰
JavaScript 开发者的异步问题。但 Promises
也远非完美。它们一直请求回调,在一些复杂的问题上仍会有些杂乱和一些难以置信的冗余。

随着 ES6 的到来(现在被称作 ES2015),除了引入 Promise
的规范,不需要请求那些数不尽的库之外,我们还有了生成器。生成器可在函数内部停止执行,这意味着可把它们封装在一个多用途的函数中,我们可在代码移动到下一行之前等待异步操作完成。突然你的异步代码可能就开始看起来同步了。

这只是第一步。异步函数因今年加入 ES2017,已进行标准化,本地支持也进一步优化。异步函数的理念是使用生成器进行异步编程,并给出他们自己的语义和语法。因此,你无须使用库来获取封装的实用函数,因为这些都会在后台处理。

运行文章中的 async/await 实例,你需要一个能兼容的浏览器。

运行兼容

在客户端,Chrome、Firefox 和 Opera 能很好地支持异步函数。

从 7.6 版本开始,Node.js 默认启用 async/await。

异步函数和生成器对比

这有个使用生成器进行异步编程的实例,用的是 Q 库:


  1. var doAsyncOp = Q.async(function* () { 
  2.  
  3.   var val = yield asynchronousOperation(); 
  4.  
  5.   console.log(val); 
  6.  
  7.   return val; 
  8.  
  9. });  

Q.async 是个封装函数,处理场景后的事情。其中 * 表示作为一个生成器函数的功能,yield 表示停止函数,并用封装函数代替。Q.async 将会返回一个函数,你可对它赋值,就像赋值 doAsyncOp 一样,随后再调用。

ES7 中的新语法更简洁,操作示例如下:


  1. async function doAsyncOp () { 
  2.  
  3.   var val = await asynchronousOperation();      
  4.  
  5.   console.log(val); 
  6.  
  7.   return val; 
  8.  
  9. };  

差异不大,我们删除了一个封装的函数和 * 符号,转而用 async 关键字代替。yield 关键字也被 await 取代。这两个例子事实上做的事是相同的:在 asynchronousOperation 完成之后,赋值给 val,然后进行输出并返回结果。

将 Promises 转换成异步函数

如果我们使用 Vanilla Promises 的话前面的示例将会是什么样?


  1. function doAsyncOp () { 
  2.  
  3.   return asynchronousOperation().then(function(val) { 
  4.  
  5.     console.log(val); 
  6.  
  7.     return val; 
  8.  
  9.   }); 
  10.  
  11. };  

这里有相同的代码行数,但这是因为 then 和给它传递的回调函数增加了很多的额外代码。另一个让人厌烦的是两个 return 关键字。这一直有些事困扰着我,因为它很难弄清楚使用 promises 的函数确切的返回是什么。

就像你看到的,这个函数返回一个 promises,将会赋值给
val,猜一下生成器和异步函数示例做了什么!无论你在这个函数返回了什么,你其实是暗地里返回一个 promise
解析到那个值。如果你根本就没有返回任何值,你暗地里返回的 promise 解析为 undefined。

链式操作

Promise 之所以能受到众人追捧,其中一个方面是因为它能以链式调用的方式把多个异步操作连接起来,避免了嵌入形式的回调。不过 async 函数在这个方面甚至比 Promise 做得还好。

下面演示了如何使用 Promise 来进行链式操作(我们只是简单的多次运行 asynchronousOperation 来进行演示)。


  1. function doAsyncOp() { 
  2.  
  3.   return asynchronousOperation() 
  4.  
  5.     .then(function(val) { 
  6.  
  7.       return asynchronousOperation(val); 
  8.  
  9.     }) 
  10.  
  11.     .then(function(val) { 
  12.  
  13.       return asynchronousOperation(val); 
  14.  
  15.     }) 
  16.  
  17.     .then(function(val) { 
  18.  
  19.       return asynchronousOperation(val); 
  20.  
  21.     }); 
  22.  
  23. }  

使用 async 函数,只需要像编写同步代码那样调用 asynchronousOperation:


  1. async function doAsyncOp () { 
  2.  
  3.   var val = await asynchronousOperation(); 
  4.  
  5.   val = await asynchronousOperation(val); 
  6.  
  7.   val = await asynchronousOperation(val); 
  8.  
  9.   return await asynchronousOperation(val); 
  10.  
  11. };  

甚至最后的 return 语句中都不需要使用 await,因为用或不用,它都返回了包含了可处理终值的 Promise。

并发操作

Promise 还有另一个伟大的特性,它们可以同时进行多个异步操作,等他们全部完成之后再继续进行其它事件。ES2015 规范中提供了 Promise.all(),就是用来干这个事情的。

这里有一个示例:


  1. function doAsyncOp() { 
  2.  
  3.   return Promise.all([ 
  4.  
  5.     asynchronousOperation(), 
  6.  
  7.     asynchronousOperation() 
  8.  
  9.   ]).then(function(vals) { 
  10.  
  11.     vals.forEach(console.log); 
  12.  
  13.     return vals; 
  14.  
  15.   }); 
  16.  
  17. }  

Promise.all() 也可以当作 async 函数使用:


  1. async function doAsyncOp() { 
  2.  
  3.   var vals = await Promise.all([ 
  4.  
  5.     asynchronousOperation(), 
  6.  
  7.     asynchronousOperation() 
  8.  
  9.   ]); 
  10.  
  11.   vals.forEach(console.log.bind(console)); 
  12.  
  13.   return vals; 
  14.  
  15. }  

这里就算使用了 Promise.all,代码仍然很清楚。

处理拒绝

Promises 可以被接受(resovled)也可以被拒绝(rejected)。被拒绝的 Promise
可以通过一个函数来处理,这个处理函数要传递给 then,作为其第二个参数,或者传递给 catch 方法。现在我们没有使用 Promise
API 中的方法,应该怎么处理拒绝?可以通过 try 和 catch 来处理。使用 async
函数的时候,拒绝被当作错误来传递,这样它们就可以通过 JavaScript 本身支持的错误处理代码来处理。


  1. function doAsyncOp() { 
  2.  
  3.   return asynchronousOperation() 
  4.  
  5.     .then(function(val) { 
  6.  
  7.       return asynchronousOperation(val); 
  8.  
  9.     }) 
  10.  
  11.     .then(function(val) { 
  12.  
  13.       return asynchronousOperation(val); 
  14.  
  15.     }) 
  16.  
  17.     .catch(function(err) { 
  18.  
  19.       console.error(err); 
  20.  
  21.     }); 
  22.  
  23. }  

这与我们链式处理的示例非常相似,只是把它的最后一环改成了调用 catch。如果用 async 函数来写,会像下面这样。


  1. async function doAsyncOp () { 
  2.  
  3.   try { 
  4.  
  5.     var val = await asynchronousOperation(); 
  6.  
  7.     val = await asynchronousOperation(val); 
  8.  
  9.     return await asynchronousOperation(val); 
  10.  
  11.   } catch (err) { 
  12.  
  13.     console.err(err); 
  14.  
  15.   } 
  16.  
  17. };  

它不像其它往 async
函数的转换那样简洁,但是确实跟写同步代码一样。如果你在这里不捕捉错误,它会延着调用链一直向上抛出,直到在某处被捕捉处理。如果它一直未被捕捉,它最终会中止程序并抛出一个运行时错误。Promise

以同样的方式运作,只是拒绝不必当作错误来处理;它们可能只是一个说明错误情况的字符串。如果你不捕捉被创建为错误的拒绝,你会看到一个运行时错误,不过如果你只是使用一个字符串,会失败却不会有输出。

中断 Promise

拒绝原生的 Promise,只需要使用 Promise 构建函数中的 reject 就好,当然也可以直接抛出错误——在 Promise 的构造函数中,在 then 或 catch 的回调中抛出都可以。如果是在其它地方抛出错误,Promise 就管不了了。

这里有一些拒绝 Promise 的示例:


  1. function doAsyncOp() { 
  2.  
  3.   return new Promise(function(resolve, reject) { 
  4.  
  5.     if (somethingIsBad) { 
  6.  
  7.       reject("something is bad"); 
  8.  
  9.     } 
  10.  
  11.     resolve("nothing is bad"); 
  12.  
  13.   }); 
  14.  
  15.  
  16.   
  17.  
  18. /*-- or --*/ 
  19.  
  20.   
  21.  
  22. function doAsyncOp() { 
  23.  
  24.   return new Promise(function(resolve, reject) { 
  25.  
  26.     if (somethingIsBad) { 
  27.  
  28.       reject(new Error("something is bad")); 
  29.  
  30.     } 
  31.  
  32.     resolve("nothing is bad"); 
  33.  
  34.   }); 
  35.  
  36.  
  37.   
  38.  
  39. /*-- or --*/ 
  40.  
  41.   
  42.  
  43. function doAsyncOp() { 
  44.  
  45.   return new Promise(function(resolve, reject) { 
  46.  
  47.     if (somethingIsBad) { 
  48.  
  49.       throw new Error("something is bad"); 
  50.  
  51.     } 
  52.  
  53.     resolve("nothing is bad"); 
  54.  
  55.   }); 
  56.  
  57. }  

一般来说,最好使用 new Error,因为它会包含错误相关的其它信息,比如抛出位置的行号,以及可能会有用的调用栈。

这里有一些抛出 Promise 不能捕捉的错误的示例:


  1. function doAsyncOp() { 
  2.  
  3.   // the next line will kill execution 
  4.  
  5.   throw new Error("something is bad"); 
  6.  
  7.   return new Promise(function(resolve, reject) { 
  8.  
  9.     if (somethingIsBad) { 
  10.  
  11.       throw new Error("something is bad"); 
  12.  
  13.     } 
  14.  
  15.     resolve("nothing is bad"); 
  16.  
  17.   }); 
  18.  
  19.  
  20.   
  21.  
  22. // assume `doAsyncOp` does not have the killing error 
  23.  
  24. function x() { 
  25.  
  26.   var val = doAsyncOp().then(function() { 
  27.  
  28.     // this one will work just fine 
  29.  
  30.     throw new Error("I just think an error should be here"); 
  31.  
  32.   }); 
  33.  
  34.   // this one will kill execution 
  35.  
  36.   throw new Error("The more errors, the merrier"); 
  37.  
  38.   return val; 
  39.  
  40. }  

在 async 函数的 Promise 中抛出错误就不会产生有关范围的问题——你可以在 async 函数中随时随地抛出错误,它总会被 Promise 抓住:


  1. async function doAsyncOp() { 
  2.  
  3.   // the next line is fine 
  4.  
  5.   throw new Error("something is bad"); 
  6.  
  7.   if (somethingIsBad) { 
  8.  
  9.     // this one is good too 
  10.  
  11.     throw new Error("something is bad"); 
  12.  
  13.   } 
  14.  
  15.   return "nothing is bad"; 
  16.  
  17. }  
  18.  
  19.   
  20.  
  21. // assume `doAsyncOp` does not have the killing error 
  22.  
  23. async function x() { 
  24.  
  25.   var val = await doAsyncOp(); 
  26.  
  27.   // this one will work just fine 
  28.  
  29.   throw new Error("I just think an error should be here"); 
  30.  
  31.   return val; 
  32.  
  33. }  

当然,我们永远不会运行到 doAsyncOp 中的第二个错误,也不会运行到 return 语句,因为在那之前抛出的错误已经中止了函数运行。

问题

如果你刚开始使用 async 函数,需要小心嵌套函数的问题。比如,如果你的 async 函数中有另一个函数(通常是回调),你可能认为可以在其中使用 await ,但实际不能。你只能直接在 async 函数中使用 await 。

比如,这段代码无法运行:


  1. async function getAllFiles(fileNames) { 
  2.  
  3.   return Promise.all( 
  4.  
  5.     fileNames.map(function(fileName) { 
  6.  
  7.       var file = await getFileAsync(fileName); 
  8.  
  9.       return parse(file); 
  10.  
  11.     }) 
  12.  
  13.   ); 
  14.  
  15. }  

第 4 行的 await 无效,因为它是在一个普通函数中使用的。不过可以通过为回调函数添加 async 关键字来解决这个问题。


  1. async function getAllFiles(fileNames) { 
  2.  
  3.   return Promise.all( 
  4.  
  5.     fileNames.map(async function(fileName) { 
  6.  
  7.       var file = await getFileAsync(fileName); 
  8.  
  9.       return parse(file); 
  10.  
  11.     }) 
  12.  
  13.   ); 
  14.  
  15. }  

你看到它的时候会觉得理所当然,即便如此,仍然需要小心这种情况。

也许你还想知道等价的使用 Promise 的代码:


  1. function getAllFiles(fileNames) { 
  2.  
  3.   return Promise.all( 
  4.  
  5.     fileNames.map(function(fileName) { 
  6.  
  7.       return getFileAsync(fileName).then(function(file) { 
  8.  
  9.         return parse(file); 
  10.  
  11.       }); 
  12.  
  13.     }) 
  14.  
  15.   ); 
  16.  
  17. }  

接下来的问题是关于把 async 函数看作同步函数。需要记住的是,async 函数内部的的代码是同步运行的,但是它会立即返回一个 Promise,并继续运行外面的代码,比如:


  1. var a = doAsyncOp(); // one of the working ones from earlier 
  2.  
  3. console.log(a); 
  4.  
  5. a.then(function() { 
  6.  
  7.   console.log("`a` finished"); 
  8.  
  9. }); 
  10.  
  11. console.log("hello"); 
  12.  
  13.   
  14.  
  15. /* -- will output -- */ 
  16.  
  17. Promise Object 
  18.  
  19. hello 
  20.  
  21. `a` finished  

你会看到 async 函数实际使用了内置的 Promise。这让我们思考 async 函数中的同步行为,其它人可以通过普通的 Promise API 调用我们的 async 函数,也可以使用它们自己的 async 函数来调用。

如今,更好的异步代码!

即使你本身不能使用异步代码,你也可以进行编写或使用工具将其编译为 ES5。 异步函数能让代码更易于阅读,更易于维护。 只要我们有 source maps,我们可以随时使用更干净的 ES2017 代码。

有许多可以将异步功能(和其他 ES2015+功能)编译成 ES5 代码的工具。 如果您使用的是 Babel,这只是安装 ES2017 preset 的例子。

作者:佚名

来源:51CTO

时间: 2024-08-27 15:22:01

用Async函数简化异步代码的相关文章

使用Scala高价函数简化代码

在Scala里,带有其他函数做参数的函数叫做高阶函数,使用高阶函数可以简化代码. 减少重复代码 有这样一段代码,查找当前目录样以某一个字符串结尾的文件: object FileMatcher { private def filesHere = (new java.io.File(".")).listFiles def filesEnding(query: String) = for (file <- filesHere; if file.getName.endsWith(quer

.NET中的“.NET研究”异步编程:使用F#简化异步编程

不管是使用yield或借助第三方类库来简化异步编程,或多或少总是感觉不那么正统,有点hack的感觉.这种感觉在实验阶段倒还可以,要是用在产品中总有点担心,即使这些类库来自权威的第三方,我不知道大家有没有跟我同样的感觉.那么这个时候我们就会想,如果在语言中直接能提供这种机制该多好呢. F#的异步工作流 在Visual Studio 2010中,新包含了一种语言:F#.F#的一大特性就是异步计算.能让你用同步的方式编写异步的代码,不用使用AsyncCallback回调将一个方法分为两段,也不用注册异

async 函数的含义和用法

本文是<深入掌握 ECMAScript 6 异步编程>系列文章的最后一篇. Generator函数的含义与用法 Thunk函数的含义与用法 co函数库的含义与用法 async函数的含义与用法 一.终极解决 异步操作是 JavaScript 编程的麻烦事,麻烦到一直有人提出各种各样的方案,试图解决这个问题. 从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底.它们都有额外的复杂性,都需要理解抽象的底层运行机制. 异步I/O不就是读取一个

[译]Async函数,让promise更友好!

Async 函数是一个非常了不起的东西,它将会在Chrome 55中得到默认支持.它允许你书写基于promise的代码,但它看起来就跟同步的代码一样,而且不会阻塞主线程.所以,它让你的异步代码看起来并没有那么"聪明"却更具有可读性. Async 函数的代码示例: async function myFirstAsyncFunction() {    try {      const fulfilledValue = await promise;    }    catch (reject

.NET中的异步编程“.NET技术”:使用F#简化异步编程

不管是使用yield或借助第三方类库来简化异步编程,或多或少总是感觉不那么正统,有点hack的感觉.这种感觉在实验阶段倒还可以,要是用在产品中总有点担心,即使这些类库来自权威的第三方,我不知道大家有没有跟我同样的感觉.那么这个时候我们就会想,如果在语言中直接能提供这种机制该多好呢. F#的异步工作流 在Visual Studio 2010中,新包含了一种语言:F#.F#的一大特性就是异步计算.能让你用同步的方式编写异步的代码,不用使用AsyncCallback回调将一个方法分为两段,也不用注册异

javascript带回调函数的异步脚本载入方法实例分析

  本文实例讲述了javascript带回调函数的异步脚本载入方法.分享给大家供大家参考.具体实现方法如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var Loader = function () { } Loader.prototype = { require: function (scripts, callback) { this.loadCount = 0; this.totalRequire

关于写异步代码测试用例的一些思考

如果说异步代码不好写是共识的话,那么写异步代码测试用例就更难了.最近我刚刚完成了一个 Flaky 测试,所以想和大家分享一些关于写异步测试用例的想法. 这篇文章里,我们会探索一个关于异步测试用例的常见问题 -- 如何强制规定某些线程的顺序,如何强制某一个线程操作早于另一些执行.通常我们并不想强行规定线程之间的顺序,因为这违背了多线程的原则,所谓多线程就是 为了做到并发,从而使得 CPU 可以根据当前资源及应用状态选择最佳的执行顺序.但是在测试中,为了确保测试结果的稳定性,又必须明确线程顺序. 测

async And await异步编程活用基础

原文:async And await异步编程活用基础 好久没写博客了,时隔5个月,奉上一篇精心准备的文章,希望大家能有所收获,对async 和 await 的理解有更深一层的理解. async 和 await 有你不知道的秘密,微软会告诉你吗? 我用我自己的例子,去一步步诠释这个技术,看下去,你绝对会有收获.(渐进描述方式,愿适应所有层次的程序员) 从零开始, 控制台 Hello World: 什么?开玩笑吧?拿异步做Hello World?? 下面这个例子,输出什么?猜猜? 1 static

jQuery中的ajax async同步和异步详解_jquery

项目中有这样一个需求,使用ajax加载数据返回页面并赋值,然后前端取出该值 这其中涉及到代码的顺序问题,有时后台还未返回数据,但已执行后面代码, 所以就会造成取不到值 $.ajax({ type: "post", url: "admin/PfmOptionRuleItem.do", success: function(data){ $("#ruleItem").val(data.ruleItem); //① } }); return $(&quo