图与例解读Async/Await

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

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

在深刻以前,咱们先简单回顾一下promise,若是对这方面概念有自信,大可自行跳过。promise

Promise

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

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

为方便说明,假定后续全部的例子都已经引入了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附加回调函数。async

下面的图表描绘了上面代码例子的执行过程:
这里写图片描述

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/

相关文章
相关标签/搜索