用图表和实例解释Await和Async

转载自:前端大全

简介

JavaScript ES7中的 async / await 让多个异步promise协同工作起来更容易。如果要按一定顺序从多个数据库或者API异步获取数据,你可能会以一堆乱七八槽的promise和回调函数而告终。而 async / await 结构让我们能用可读性强、易维护的代码更加简洁的实现这些逻辑。

本教程用图表和简单示例讲解了JavaScript中 async / await 的语法和语义。

在深入之前,我们先简单回顾一下promise.如果你已经对JS的promise有所了解,可放心大胆地跳过这一部分。

Promises

在JavaScript中,promise代表非阻塞异步执行的抽象概念。如果你熟系Java的Future、C#的Task,你会发现promise跟他们很像。

Promise一般用于网路和I/O操作,比如读取文件,或者创建HTTP请求。我们可以创建异步promise,然后用then添加一个回调函数,当promise结束后会触发这个回调函数,而非阻塞住当前“线程”。回调函数本身也可以返回一个promise对象,所以我们能够链式调用promise。

为了简单起见,我们假设后面所有示例都已经像这样安装并加载了request-promise类库:

var rp = require('request-promise');

现在我们就可以像这样创建一个返回promise对象的简易HTTP GET请求:

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结束之前就阻塞当前线程。对于JavaScript,我们没法等待Promise执行完。在promise后面编排代码的唯一方法使用then给它添加回调函数。

下图描述了本例的计算过程:

这里写图片描述

Promise的计算过程。正在执行的“线程”无法等待promise执行完成。

通过then添加的回调函数只有当promise成功时才会执行。如果它失败了(比如由于网络错误),回调函数不会执行。你可以用catch再附加一个回调函数来处理失败的promise:

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

最后,为了测试,我们可以用Promise.resolve和Promise.reject很容易的创建执行成功和失败的“傻瓜”promise:

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


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

问题来了——组合promise

只用一个promise很容易搞定。但是,当需要针对复杂异步逻辑编程时,我们很可能最后要同时用好几个promise对象。写一堆then语句和匿名回调很容易搞的难以控制。

假如,假设我们需要编程解决如下需求:

  1. 创建HTTP请求,等待请求结束并打印出结果;
  2. 再创建两个并行HTTP请求;
  3. 等着两个请求结束后,打印出他们的结果。

下面这段代码示范了如果解决此问题:

// 第一次调用
const call1Promise = rp('http://example.com/');

call1Promise.then(result1 => {
    // 第一个请求完成后会执行
    console.log(result1);
    const call2Promise = rp('http://example.com/');
    const call3Promise = rp('http://example.com/');

    return Promise.all([call2Promise, call3Promise]);
}).then(arr => {
    // 两个 promise 都结束后会执行
    console.log(arr[0]);
    console.log(arr[1]);
})

我们开头创建了第一个HTTP请求,并且加了个完成时候运行的回调(1-3行)。在这个回调函数里,我们为随后的HTTP请求创建了另外两个promise(8-9行)。这两个promise同时执行,我们需要加一个能等它们都完成后才执行的回调函数。因此,我们需要用Promise.all将它们组合到同一个promise中(11行),它们都结束后这个promise才算完成。这个回调返回的是promise对象,所以我们要再加一个then回调函数来打印结果(12-16行)。

下面描述了这一计算流程:

这里写图片描述

Promise组合的计算过程。我们用Promise.all将两个并行的promise组合到一个promise中。

对于这个简单的例子,我们最后用两个then回调函数,并且不得不用Promise.all来让两个并行的promise同时执行。如果我们必须执行更多异步操作,或者加上错误处理会怎么样呢?这种方法最后很容易产生一堆乱七八槽的then,Promise.all和回调函数。

Async方法

Async是定义返回promise对象函数的快捷方法。

例如,下面这两种定义是等价的:

function f() {
    return Promise.resolve('TEST');
}

// asyncF 和 f 是等价的
async function asyncF() {
    return 'TEST';
}

类似地,抛出异常的async方法等价于返回拒绝promise的方法:

function f() {
    return Promise.reject('Error');
}

// asyncF 和 f 是等价的
async function asyncF() {
    throw 'Error';
}

Await

我们创建了promise但不能同步等待它执行完成。我们只能通过then传一个回调函数。不允许等待promise是为了鼓励开发非阻塞代码。否则,开发者们总会忍不住执行阻塞操作,因为那比使用promise和回调更简单。

然而,为了让promise能同步执行,我们需要让它们等待彼此完成。换句话说,如果一个操作是异步的(即封装在promise中),它应该能够等待另一个异步操作执行完。但是JavaScript解释器怎么知道一个操作是否在promise中运行呢?

答案就再async这个关键词中。每个async方法都返回一个promise对象。因此,JavaScript解释器就明白所有async方法中的操作都被封装在promise里异步执行。所以解释器能够允许它们能带其他promise执行完。

下面引入await关键词。它只能被用在async方法中,让我们能同步等待promise执行完。如果在async函数外使用promise,我们仍然需要then回调函数:

async function f(){
    // response 就是 promise 执行成功的值
    const response = await rp('http://example.com/');
    console.log(response);
}

// 不能在 async 方法外面用 await
// 需要使用 then 回调函数……
f().then(() => console.log('Finished'));

现在我们来看如何解决上一节的问题:

// 将解决方法封装到 async 函数中
async function solution() {

    // 等待第一个 HTTP 请求并打印出结果
    console.log(await rp('http://example.com/'));


    // 创建两个 HTTP 请求,不等它们执行完 —— 让他们同时执行
    const call2Promise = rp('http://example.com/');  // Does not wait!
    const call3Promise = rp('http://example.com/');  // Does not wait!


    // 创建完以后 —— 等待它们都执行完
    const response2 = await call2Promise;
    const response3 = await call3Promise;

    console.log(response2);
    console.log(response3);
}


// 调用这一 async 函数
solution().then(() => console.log('Finished'));

上面的这段代码中,我们把解决方法封装到async函数中。这让我们能直接对立面的promise使用await关键字,所以不再需要使用then回调函数。最后,调用async函数,它简单地创建了一个promise对象,这个promise封装了调用其他promise的逻辑。

当然,在第一个例子(没有用 async / await)中,两个promise会被同时出发。这段代码也一样(7-8行)。注意,直到第11-12行我们才使用await,将程序一直阻塞到两个promise执行完成。然后我们就能断定上例中两个promise都成功执行了(和使用Promise.all().then()类似)。

这背后的计算过程跟上一节给出的基本相当。但是代码可读性更强、更易于理解。

实际上,async / await 在底层转换成了promise和then回调函数。也就是说,这是使用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的,所以它会跟着它的调用方同时执行:

Await的计算过程

函数f开始运行并创建了一个promise对象。就在那一刻,函数中剩下的部分被封装到一个回调函数中,并在promise结束后执行。

错误处理

前面大部分例子中,我们都假设promise执行成功,因此在promise上使用await会返回值。如果我们进行await的promise失败了,async函数就会发生异常。我们可以用标准的try / catch 来处理这种情况:

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

Async函数不会处理异常,不管异常是拒绝的promise还是其他bug引起的,它都会返回一个拒绝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 函数中,然后在非异步代码中调用。这让我尽可能少地写 try / catch 回调。

Async / await 结构是让使用 promise 更简练的语法糖。每一个 async / await 结构都可以写成普通 promise. 归根结底,这是一个编码风格和简洁的问题。

关于说明并发和并行有区别的资料,可以查看 Rob Pike 关于这个问题的讨论,或者我这篇文章。并发是指将独立进程(通常意义上的进程)组合在一起工作,而并行是指真正同时处理多个任务。并发关乎应用设计和架构,而并行关乎实实在在的执行。

我们拿一个多线程应用来举例。应用程序分离成线程明确了它的并发模型。这些线程在可用内核上的映射定义了其级别或并行性。并发系统可以在单个处理器上高效运行,在这种情况下,它并不是并行的。

这里写图片描述

并发VS并行

从这个意义上说,promise 让我们能够将程序分解成并发模块,这些模块可能会也可能不会并行执行。Javascript 实际否并行执行取决于具体实现方法。例如,Node JS 是单线程的,如果 promise 是计算密集型(CPU bound)那就不会有并行处理。但是,如果你用 Nashorn 之类的东西把代码编译成 java 字节码,理论上可能能够将计算密集型的 promise 映射到不同 CPU 核上,从而达到并行效果。所以我认为,promise(不管是普通的还是用了 async / await 的)组成了 JavaScript 应用的并发模块。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值