c# await 获得文件大小_图与例解读Async/Await

概述

JavaScript ES7的async/await语法让异步promise操作起来更方便。如果你需要从多个数据库或者接口按顺序异步获取数据,你可能最终写出一坨纠缠不清的promise与回调。然而使用async/await可以让我们用更加可读、可维护的方式来表达这种逻辑。

这篇教程以图表与简单例子来阐述JS async/await的语法与运行机理。

在深入之前,我们先简单回顾一下promise,如果对这方面概念有自信,大可自行跳过。


Promise

在JS的世界里,一个promise抽象表达一个非阻塞(阻塞指一个任务开始后,要等待该任务执行结果产生之后才继续执行后续任务)的异步流程,类似于Java的Futrue或者C#的Task。

Promise最典型的使用场景是网络或其他I/O操作(如读取一个文件或者发送一个HTTP请求)。与其阻塞住当前的执行“线程”,我们可以产生一个异步的promise,然后用then方法来附加一个回调,用于执行该promise完成之后要做的事情。回调自身也可以返回一个promise,如此我就可以将多个promise串联。

为方便说明,假定后续所有的例子都已经引入了request-promise库:

var rp = require('request-promise');

然后我们就可以如此发送一个简单的HTTP GET请求并获得一个promise返回值:

const promise = rp('http://example.com/')

现在来看个例子:

console.log('Starting Execution');const promise = rp('http://example.com/');promise.then(result => console.log(result));console.log("Can't know if promise has finished yet...");

我们在第3行产生了一个promise,然后在第4行附上了一个回调函数。返回的promise是异步的,所以当执行的第6行的时候,我们无法确定这个promise有没有完成,多次执行可能有不同的结果(译者:浏览器里执行多少次,这里promise都会是未完成状态)。概括来说,promise之后的代码跟promise自身是并发的(译者:对这句话有异议者参见本文最后一节的并发说明)

并不存在一种方法可以让当前的执行流程阻塞直到promise完成,这一点与Java的Futrue.get相异。JS里,我们无法直接原地等promise完成,唯一可以用于提前计划promise完成后的执行逻辑的方式就是通过then附加回调函数。

下面的图表描绘了上面代码例子的执行过程:

3f63b5c1a4e875970d9cd170a29d77dd.png

Promise的执行过程,调用“线程”无法直接等待promise结果。唯一规划promise之后逻辑的方法是使用then方法附加一个回调函数。

通过then 附加的回调函数只会在promise成功是被触发,如果失败了(比如网络异常),这个回调不会执行,处理错误需要通过catch方法:

rp('http://example.com/'). then(() => console.log('Success')). catch(e => console.log(`Failed: ${e}`))

最后,为了方便试验功能,我们可以直接创建一些“假想”的promise,使用Promise.resolve生成会直接成功或失败的promise结果:

069de448e185ae39474e2aee31e716a7.png

问题——组合多个Promise

只使用一个单次的promise非常简单。然而如果我们需要编写一个非常复杂了异步逻辑,我们可能需要将若干个promise组合起来。写许多的then语句以及匿名函数很容易失控。

比如,我们需要实现以下逻辑:

  1. 发起一个HTTP请求,等待结果并将其输出
  2. 再发起两个并发的HTTP请求
  3. 当两个请求都完成时,一起输出他们

下面的代码演示如何达到这个要求:

a33300b9f5cd449bee9aba0a59b262a5.png

我们先呼叫第一次HTTP请求,然后预备一个在它完成时执行的回调(第1-3行)。在回调里,我们为另外两次请求制造了promise(第8-9行)。这两个promise并发运行,我们需要计划一个在两个都完成时执行的回调,于是,我们通过Promise.all(第11行)来讲他们合并。这第一个回调的返回值是一个promise,我们再添加一个then来输出结果(第12-16行)。

以下图标描绘这个计算过程:

608825bffdac2f78edef9ba856fa5478.png

将promise组合的计算过程。使用“Promise.all”将两个并发的promise合并成一个。

为了一个简单的例子,我们最终写了两个then回调以及一个Promise.all来同步两个并发promise。如果我们还想再多做几个异步操作或者添加一些错误处理会怎样?这种实现方案最终很容变为纠缠成一坨的then、Promise.all以及回调匿名函数。


Async函数

一个async函数是定义会返回promise的函数的简便写法。

比如,以下两个定义是等效的:

function f() { return Promise.resolve('TEST');}// asyncF is equivalent to f!async function asyncF() { return 'TEST';}

相似地,会抛出错误的async函数等效于返回将失败的promise的函数:

function f() { return Promise.reject('Error');}// asyncF is equivalent to f!async function asyncF() { throw 'Error';}

Await

以前,当我们产生一个promise,我们无法同步地等待它完成,我们只能通过then注册一个回调函数。不允许直接等待一个promise是为了鼓励开发者写非阻塞的代码,不然开发者会更乐意写阻塞的代码,因为这样比promise和回调简单。

然而,为了同步多个promise,我们需要它们互相等待,换句话说,如果一个操作本身就是异步的(比如,用promise包装的),它应该具备能力等待另一个异步操作先完成。但是JS解释器如何知道一个操作是不是在一个promise里的?

答案就是async关键字,所有的async函数一定会返回一个promise。所以,JS解释器也可以确信async函数里操作是用promise包装的异步过程。于是也就可以允许它等待其他promise。

键入await关键字,它只能在async函数内使用,让我们可以等待一个promise。如果在async函数外使用promise,我们依然需要使用then和回调函数:

1a0c0549a3ecb890ed8d5d6879e0ee5d.png

现在我们来看看我们可以如何解决之前提到的问题:

d19af8d34d9c13101f0c0567eaa77c1f.png

上面的片段,我们将逻辑分装在一个async函数里。这样我们就可以直接对promise使用await了,也就规避了写then回调。最后我们调用这个async函数,然后按照普通的方式使用返回的promise。

要注意的是,在第一个例子里(没有async/await),后面两个promise是并发的。所以我们在第7-8行也是如此,然后直到11-12行才用await来等待两个promise都完成。这之后,我们可以确信两个promise都已经完成(与之前Promise.all(...).then(...)类似)。

计算流程跟之前的图表描绘的一样,但是代码变得更加已读与直白。

事实上,async/await其实会翻译成promise与then回调(译者:babel其实是翻译成generator语法,再通过类似co的函数运行,co内部运行机制离不开promise)。每次我们使用await,解释器会创建一个promise然后把async函数的后续代码放到then回调里。

我们来看看以下的例子:

async function f() { console.log('Starting F'); const result = await rp('http://example.com/'); console.log(result);}

f函数的内在运行过程如下图所描绘。因为f标记了async,它会与它的调用者“并发”:

69d184c47d3159a23bc27a3fe9779cab.png

函数f启动并产生一个promise。在这一刻,函数剩下的部分都会被封装到一个回调函数里,并被计划在promise完成之后执行。


错误处理

在之前的例子里,我们大多假定promise会成功,然后await一个promise的返回值。如果我们等待的promise失败了,会在async函数里产生一个异常,我们可以使用标准的try/catch来处理它:

bdb7d3fbb8ba5c3df58fd756da39ad78.png

如果async函数不处理这个异常,不管是这异常是因为promise是被reject了还是其他的bug,这个函数都会返回一个被reject掉的promise:

2e4308b42da8cdeb651b4e136b5a1fa3.png

这就让我们可以使用熟悉的方式来处理错误。


扩展说明

async/await是一个对promise进行补充的语法部件,它能让我们写更少的重复代码来使用promise。然而,async/await并不能彻底取代普通的promise。比如,如果我们在一个普通的函数或者全局作用域里使用一个async函数,我们无法使用await,也就只能求助于原始的promise用法:

async function fAsync() { // actual return value is Promise.resolve(5) return 5;}// can't call "await fAsync()". Need to use then/catchfAsync().then(r => console.log(`result is ${r}`));

我通常会把大部分的异步逻辑封装在一个或少量几个async函数里,然后在非async的代码区域里使用,这样就可以尽量减少书写then或catch回调。

async / await是让promise用起来更简洁的语法糖。所有的async / await都可以用普通的promise来实现。所有总结来说,这只是个代码样式与简洁的问题。

学院派的人会指出,并发与并行是有区别的(译者:所以前文都是说并发,而非并行)。

并发是组合多个独立过程来一起工作,并行是多个过程同时执行。并发是体现在应用的结构设计,并行是实际执行的方式。

我们来看看一个多线程应用的例子。将应用分割成多个线程是该应用并发模型的定义,将这些线程放到可用的cpu核心上执行是确立它的并行。一个并发的系统也可以在一个单核处理器上正常运行,但这种情况并不是并行。

69a7880530c40a8ab90ab7c692f0daf7.png

并发(concurrent)与并行(parallel)

以这种方式理解,promise可以将一个程序分解成多个并发的模块,它们或许,也可能并不会并行执行。JS是否并行执行要看解释器自身的实现。

比如,NodeJS是单线程的,如果一个promise里有大量的CPU操作(非I/O操作),你可能感受不到太多并行。然而如果你用像nashorn这样的工具把代码编译成java字节码,理论上你可以把繁重的CPU操作放到其他内核上来获得平行效果。

于是在我的观点中,promise(不管是裸的还是有async/await)只是作用于定义JS应用的并发模型(而非确定逻辑是否会并行运行)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值