以图表和示例的角度解读async/await

介绍

ES7中,async/await 语法使异步promise的协调变得很简单。若是你须要以特定顺序异步获取来自多个数据库或API的数据,可使用杂乱的promise或回调函数。async/await使咱们能够更简便地处理这种逻辑,代码的可读性和可维护性也更好。java

在该教程中,咱们用图表和一些简单的例子来解释async/await的语法和语义。
开始讲解以前,咱们先对promise进行一个简单的概述,若是你对promise已经很熟悉了,能够跳过该部份内容。node

Promises

在js中,promise表示抽象的非阻塞异步执行。js中的promise与Java中的 Future或C#中的Task很类似。数据库

promise一般用于网络和I/O操做-例如,读取文件,发起HTTP请求。为了避免阻塞当前执行线程,咱们建立一个异步promise,使用then方法绑定一个回调函数,该回调函数会在promise完成后触发。回调函数自己也能够返回一个promise,因此promise能够高效的链式调用。编程

简单起见,全部的例子中咱们都假定request-promise库已经安装和加载完成了,以下所示: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方法绑定回调函数。异步

下图描述了该示例的计算过程:
图片描述async

then方法中绑定的回调函数只有当promise成功的时候才会调用。若是promise失败的话(例如,因为网络错误),回调不会执行。为了处理失败的promise,须要经过catch绑定另外一个回调函数。

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

最后,为了测试一下效果,咱们经过Promise.resolvePromise.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语句和匿名回调函数很容易让代码变得不可维护。

例如,咱们要编写一个以下功能的代码:

  1. 发起一个HTTP请求,等待完成后,打印出结果

  2. 而后发起两个并行的HTTP请求;

  3. 后两个请求都完成后,打印出他们的结果。

下面的代码片断演示了上述功能的实现:

// 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。试想若是咱们执行更多的异步操做或者增长错误处理函数呢?这种方式很容易让代码变成一堆杂乱的thenpromise.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应用程序的并发模型。

相关文章
相关标签/搜索