介绍
ES7中,async/await
语法使异步promise
的协调变得很简单。如果你需要以特定顺序异步获取来自多个数据库或API的数据,可以使用杂乱的promise或回调函数。async/await
使我们可以更简便地处理这种逻辑,代码的可读性和可维护性也更好。
在该教程中,我们用图表和一些简单的例子来解释async/await
的语法和语义。
开始讲解之前,我们先对promise
进行一个简单的概述,如果你对promise已经很熟悉了,可以跳过该部分内容。
Promises
在js中,promise表示抽象的非阻塞异步执行。js中的promise与Java中的 Future
或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...");
在第三行,我们创建了一个promise
,然后我们在第四行中为其绑定了一个回调函数。由于promise
是异步执行的
,所以执行到第六行时,我们不确定promise有没有完成。多次运行上面的代码,得到的结果可能每次都不一样。更通俗地讲,promise后面的代码和promise是并行运行的。
在promise完成之前,没有办法中断当前的操作序列。这与Java中的 Future.get
是不同的,Future.get
允许我们中断当前的线程直到Future
完成。js中,我们不会轻易地等待promise执行完成。在promise完成之后安排代码的唯一方式是通过then
方法绑定回调函数。
下图描述了该示例的计算过程:
then
方法中绑定的回调函数只有当promise成功的时候才会调用。如果promise失败的话(例如,由于网络错误),回调不会执行。为了处理失败的promise,需要通过catch
绑定另一个回调函数。
rp('http://example.com/').
then(() => console.log('Success')).
catch(e => console.log(`Failed: ${e}`))
最后,为了测试一下效果,我们通过Promise.resolve
和Promise.reject
简单地生成成功和失败的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是很简单的。可是,我们编写复杂的异步逻辑时,可能需要组合使用多个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行)。在回调函数中,我们又相继发起两个HTTP请求生成了两个promise。这两个promise并行运行;当他们都执行完后,我们还需要为其绑定一个回调函数。因此,我们用promise.all
将这两个promise组合成一个promise, 只有当他们都完成后,这个promise才会完成。由于第一个回调函数的结果是promise,因此我们链式地调用另一个then方法和回调函数输出最终结果。
下图描述了这个执行过程:
对于这么简单的例子,我们就用了两个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';
}
相似地,在异步函数抛出异常与返回一个reject promise对象的函数等价:
function f() {
return Promise.reject('Error');
}
// asyncF is equivalent to f!
async function asyncF() {
throw 'Error';
}
Await
我们不能同步等待promise的完成。只能通过then
方法传入一个回调函数。我们鼓励非阻塞编程,因此同步等待promise是不允许的。否则,开发者会产生编写同步脚本的想法,毕竟同步编程要简单的多。
但是,为了同步promise我们需要允许他们等待彼此的完成。换句话说,如果操作是异步的(也就是说包裹在promise中),它应该可以等待其他异步操作的完成。但是,js解析器怎么知道操作是否跑在promise中?
答案是async
关键字。每个async
函数返回一个promise。因此,js解析器知道所有的操作都位于async
函数中,并将所有的代码包裹在promise中异步地执行。所以,async
函数,允许操作等待其他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'));
现在我们看一下前面的那个例子如何用async/await
进行改写:
/ 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
函数中。我们可以直接await
promise的执行,省掉了then
回调函数。最后,我们只需要调用async
函数。它封装了调用其他promise的逻辑,并返回一个promise。
实际上在上面的例子中,promise是并行触发的。本例中也一样(7-8行)。注意第12-13行我们使用了await
阻塞主线程,等待所有的promise执行完成。后面,我们看到promise都完成了,和前面的例子类似(promise.all(...).then(...))。
其执行流程与前例的流程是相等的。但是,代码变得更具可读性和简洁。
底层实现上,await/async实际上转换成了promise,换句话说,await/async
是promise的语法糖。每次我们使用await
时,js解析器会生成一个promise,并将async
函数中的剩余代码放到then回调中去执行。
思考下面的例子:
async function f() {
console.log('Starting F');
const result = await rp('http://example.com/');
console.log(result);
}
下面描述函数f的基本计算过程。由于f是异步的,它会与调用方并行执行:
函数f开始执行,遇到await
后生成一个promise。此时,函数的其余部分被封装在回调中,并在promise完成后执行。
错误处理
前面的大部分例子中,我们都是假设promise成功完成了。因此,等待promise返回一个值。如果我们等待的promise失败了,在async
函数中会导致一个异常。我们可以使用标准的try/catch
来捕获和处理它。
async function f() {
try {
const promiseResult = await Promise.reject('Error');
} catch (e){
console.log(e);
}
}
如果async
函数没有处理异常,不管是promise reject了,还是产生了其他bug,它都会返回一个rejected的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))
这给我们提供了一种简便的方法,通过已知的异常处理机制来处理被rejected的promise。
讨论
async/await
在语言结构上是对promise的补充。但是,async/await
并不能取代纯promise的需求。例如,在正常函数和全局作用域我们不能使用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
的数量。
学者们指出,并发性和并行性是有区别的。并发性是指将独立的进程(一般意义上的进程)组合在一起,而并行实际上是同时执行多个进程。并发性是关于应用程序设计和结构的,而并行性是关于实际执行的。
我们以一个多线程应用程序为例。应用程序分离到线程定义了它的并发模型。这些线程在可用内核上的映射定义了它的级别或并行性。并发系统可以在单个处理器上高效运行,在这种情况下,它不是并行的。
就此而言,promise允许我们将一个程序分解为并行的并发模块,也可以不并行运行。实际的JavaScript执行是否并行取决于实现。例如,Node Js是单线程的,如果一个promise是CPU绑定的,你就不会看到太多的并行性。然而,如果你通过像Nashorn这样的东西把你的代码编译成java字节码,理论上你可能能够在不同核心上映射CPU绑定的promise,并且实现并行性。因此,在我看来,promise(无论是普通的或通过await/async)构成了JavaScript应用程序的并发模型。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。