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. 生日蛋糕(dfs+剪枝)

    生日蛋糕 POJ - 1190 题目: 7月17日是Mr.W的生日,ACM-THU为此要制作一个体积为Nπ的M层生日蛋糕,每层都是一个圆柱体.  设从下往上数第i(1 <= i <= M) ...

  2. 网络流强化-POJ2516

    k种货物分开求解最小费用最大流,主要减少了寻找最短路的时间. #include<queue> #include<cstdio> #include<cstring> ...

  3. Win7 64位注册32位DLL

    记忆力越来越差,备忘. 参考地址 https://support.microsoft.com/en-us/help/249873/how-to-use-the-regsvr32-tool-and-tr ...

  4. Django 安装、创建第一个项目

    一.版本 Django 版本对应的 Python 版本:   Django 版本 Python 版本 1.8 2.7, 3.2 , 3.3, 3.4, 3.5 1.9, 1.10 2.7, 3.4, ...

  5. Mysql 在 select 查询时追加(添加)一个字段并指定值

    在特定时候,在 mysql 的查询结果中我们需要追加一个字段来实现某些特定的功能,这时我们可以用到以下语法来实现 值 as 字段比如我们需要给这个查询结果追加一个 xx 字段并赋值为 null ,可以 ...

  6. bootstrap-select、datatables插件使用

    1.引入样式文件 <%--引入bootstrap_select样式--%> <link rel="stylesheet" type="text/css& ...

  7. Python入门习题5.蒙特卡罗方法计算圆周率

    #CalPi.py from random import random from math import sqrt from time import clock DARTS = 10000000 hi ...

  8. 部署Lighttpd到252板子

    1.先到lighttpd官网下载对应版本的软件包: 如: lighttpd-1.4.30.tar.gz 2. 将压缩包解压到任意目录得到文件夹 lighttpd-1.4.30 3. 执行配置命令:  ...

  9. 解决WordPress设置错误的url网站不能访问

    由于设置了未备案的域名,而导致网站访问不了了.同时WordPress是使用docker搭建部署的 docker ps #查看 docker 容器 CONTAINER ID IMAGE COMMAND ...

  10. Codecraft-17 and Codeforces Round #391 - B

    题目链接:http://codeforces.com/contest/757/problem/B 题意:给定n个数字,问最多能选个多少个数字使得选出来的数字的gcd!=1. 思路:由于数字最大为1e5 ...