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附加回调函数。

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

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 结果:

const success = Promise.resolve('Resolved');
// Will print "Successful result: Resolved"
success.
then(result => console.log(`Successful result: ${result}`)).
catch(e => console.log(`Failed with: ${e}`)) const fail = Promise.reject('Err');
// Will print "Failed with: Err"
fail.
then(result => console.log(`Successful result: ${result}`)).
catch(e => console.log(`Failed with: ${e}`))

问题——组合多个Promise

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

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

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

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

// Make the first call
const call1Promise = rp('http://example.com/'); call1Promise.then(result1 => {
// Executes after the first request has finished
console.log(result1); const call2Promise = rp('http://example.com/');
const call3Promise = rp('http://example.com/'); return Promise.all([call2Promise, call3Promise]);
}).then(arr => {
// Executes after both promises have finished
console.log(arr[0]);
console.log(arr[1]);
})

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

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

将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和回调函数:

async function f(){
// response will evaluate as the resolved value of the promise
const response = await rp('http://example.com/');
console.log(response);
} // We can't use await outside of async function.
// We need to use then callbacks ....
f().then(() => console.log('Finished'));

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

// Encapsulate the solution in an async function
async function solution() {
// Wait for the first HTTP call and print the result
console.log(await rp('http://example.com/')); // Spawn the HTTP calls without waiting for them - run them concurrently
const call2Promise = rp('http://example.com/'); // Does not wait!
const call3Promise = rp('http://example.com/'); // Does not wait! // After they are both spawn - wait for both of them
const response2 = await call2Promise;
const response3 = await call3Promise; console.log(response2);
console.log(response3);
} // Call the async function
solution().then(() => console.log('Finished'));

上面的片段,我们将逻辑分装在一个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,它会与它的调用者“并发”:



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

错误处理

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

async function f() {
try {
const promiseResult = await Promise.reject('Error');
} catch (e){
console.log(e);
}
}

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

async function f() {
// Throws an exception
const promiseResult = await Promise.reject('Error');
} // Will print "Error"
f().
then(() => console.log('Success')).
catch(err => console.log(err)) async function g() {
throw "Error";
} // Will print "Error"
g().
then(() => console.log('Success')).
catch(err => console.log(err))

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

扩展说明

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/catch
fAsync().then(r => console.log(`result is ${r}`));

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

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

学院派的人会指出,并发与并行是有区别的(译者:所以前文都是说并发,而非并行)。参见Rob Pike的讲话或者我之前的博文。并发是组合多个独立过程来一起工作,并行是多个过程同时执行。并发是体现在应用的结构设计,并行是实际执行的方式。

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



以这种方式理解,promise可以将一个程序分解成多个并发的模块,它们或许,也可能并不会并行执行。JS是否并行执行要看解释器自身的实现。比如,NodeJS是单线程的,如果一个promise里有大量的CPU操作(非I/O操作),你可能感受不到太多并行。然而如果你用像nashorn这样的工具把代码编译成java字节码,理论上你可以把繁重的CPU操作放到其他内核上来获得平行效果。于是在我的观点中,promise(不管是裸的还是有async/await)只是作用于定义JS应用的并发模型(而非确定逻辑是否会并行运行)。

关于本文

译者:@安秦

译文:https://zhuanlan.zihu.com/p/30500864

作者:@Nikolay

原文:http://nikgoozev.com/2017/10/01/async-await/

图与例解读Async/Await的更多相关文章

  1. 以Python为例的Async / Await的编程基础

    来源:Redislabs 作者:Loris Cro 翻译:Kevin (公众号:中间件小哥) 近年来,许多编程语言都在努力改进它们的并发原语.Go 语言有 goroutines,Ruby 有 fibe ...

  2. 【转】以Python为例的Async / Await的编程基础

    转, 原文:https://www.cnblogs.com/middleware/p/11996731.html 以Python为例的Async / Await的编程基础 -------------- ...

  3. 8张图让你一步步看清 async/await 和 promise 的执行顺序

    摘要: 面试必问 原文:8张图帮你一步步看清 async/await 和 promise 的执行顺序 作者:ziwei3749 Fundebug经授权转载,版权归原作者所有. 为什么写这篇文章? 说实 ...

  4. 8 张图帮你一步步看清 async/await 和 promise 的执行顺序(转)

    https://mp.weixin.qq.com/s?__biz=MzAxODE2MjM1MA==&mid=2651555491&idx=1&sn=73779f84c289d9 ...

  5. 从Thread,ThreadPool,Task, 到async await 的基本使用方法解读

    记得很久以前的一个面试场景: 面试官:说说你对JavaScript闭包的理解吧? 我:嗯,平时都是前端工程师在写JS,我们一般只管写后端代码. 面试官:你是后端程序员啊,好吧,那问问你多线程编程的问题 ...

  6. [转] 以 async/await 为例,说明 babel 插件怎么搭

    你一定碰到过这些库 babel-polyfill 项目地址:https://github.com/babel/babel/blob/master/packages/babel-polyfill 通过两 ...

  7. ASP.NET 中的 Async/Await 简介

    本文转载自MSDN 作者:Stephen Cleary 原文地址:https://msdn.microsoft.com/en-us/magazine/dn802603.aspx 大多数有关 async ...

  8. ASP.NET 上的 Async/Await 简介

    原文链接 大多数有关 async/await 的在线资源假定您正在开发客户端应用程序,但在服务器上有 async 的位置吗?可以非常肯定地回答“有”.本文是对 ASP.NET 上异步请求的概念性概述, ...

  9. 【限时免费】AppBoxCore - 细粒度权限管理框架(EFCore+RazorPages+async/await)!

    目录 前言 全新AppBoxCore RazorPages 和 TagHelpers 技术架构 页面处理器和数据库操作的异步调用 Authorize特性和自定义权限验证过滤器 Authorize登录授 ...

随机推荐

  1. 刚性方程 Stiff equation

    In mathematics, a stiff equation is a differential equation for which certain numerical methods for ...

  2. DHCP原理

    一台主机的ip地址可用通过两种方式来设置.1 手动输入:2 自动向DHCP服务器获取.手动输入会出现错误,比如输入一个已经分配的ip地址,当内网机器只有几台,十几台还可以忍受,如果是几百台呢,不可能一 ...

  3. C# 做延迟,但系统又能同时能执行其它任务

    private void Delay(int Millisecond) //使用时直接调用即可 { DateTime current = DateTime.Now; while (current.Ad ...

  4. rpm --qf 命令

    1. 环境准备: sudo apt-get install rpm (Ubuntu系统) wget ftp://rpmfind.net/linux/fedora-secondary/developme ...

  5. 大数据学习笔记之Zookeeper(四):Zookeeper实战篇(二)

    文章目录 4.1 分布式安装部署 4.2 客户端命令行操作 4.3 API应用 4.3.1 eclipse环境搭建 4.3.2 创建ZooKeeper客户端: 4.3.3 创建子节点 4.3.4 获取 ...

  6. 16/7/11_PHP-文件系统

    读取文件内容 PHP具有丰富的文件操作函数,最简单的读取文件的函数为file_get_contents,可以将整个文件全部读取到一个字符串中. $content = file_get_contents ...

  7. Discuz升级 Database Error : pre_common_syscache ADD PRIMARY KEY (cname)【解决办法】

    错误码: 1068Multiple primary key defined Execution Time : 00:00:00:000Transfer Time : 00:00:00:000Total ...

  8. 边界安全 - CDN/DMZ/网络协议

    CDN   工具 - LuManager CDN DMZ 网络协议 - DNS Win7下搭建DNS服务器 - BIND 根域 顶级域(即相关国家域名管理机构的数据库,如中国的CNNIC) com n ...

  9. Waiter.js

    var Waiter = function() {     var dfd = [],         doneArr = [],         failArr = [],         slic ...

  10. Spring事务传播及数据库事务操作

    从Spring 事务配置说起 先看看Spring 事务的基础配置 <aop:aspectj-autoproxy proxy-target-class="true"/> ...