图解 Await 和 Async

  1. 简介
  2. Promise
  3. 问题:组合 Promise
  4. Async 函数
  5. Await
  6. 错误处理
  7. 讨论

JavaScript ES7中的 async/await 使得协调异步 promises 变得更容易。如果你需要从多个数据库或API异步获取数据,则可以使用 promise 和回调函数。async / await 使我们更简洁地表达这种逻辑,并完成更易读和可维护的代码。

本教程将使用图表和简单示例来解释JavaScript中 的async / await 语法。

在讲解之前,我们从 promises 的简要概述开始。如果你已经了解了JS中的 promises,请随时跳过本节。

在JavaScript中,promises 代表非阻塞异步执行的抽象对象。JS中的 promises 与Java中的 FutureC#的 Task 类似,如果你了解它们的话很容易理解。

Promises 通常用于网络和 I/O 操作 ,例如读取文件或者发出HTTP请求。我们可以产生一个异步 promise ,并使用 then 的方法来附加一个回调函数,这个回调函数当 promise 完成时将会被触发,这种方法不会阻止当前的“线程”执行。回调函数本身可以返回 promise ,使我们可以有效地链接 promises。

为了容易理解,在所有示例中,我们假设 request-promise 库已经安装并加载为:

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

我们做一个简单的HTTP GET请求,返回一个 promise :

const promise = rp('http://example.com/')

现在,让我们来看一个例子:

console.log('Starting Execution');

console.log("Can't know if promise has finished yet...");

我们在第3行产生了一个新的 Promise,然后在第4行 附加一个回调函数。因为promise 是异步的,所以当我们到达第6行时,我们不知道 promise 是否已经完成。如果我们多次运行代码,我们可能会每次得到不同的结果。换句话说,任何 promise 之后的代码都是与 promise 同时运行的。

promise 完成之前,并没有办法阻止当前的操作顺序。 这与Java中的 Future.get 不同,其允许我们阻止当前线程,然后之后完成。在JavaScript中,我们不能等待 promise。在 promise 之后调度代码的唯一方法是通过 then 附加回调函数。

下图描绘了该示例的计算过程:

示例的计算过程

######promise 的计算过程。呼叫“线程”不能等待 promise 。在 promise 之后调度代码的唯一方法是通过 then 方法指定回调函数。###### 当 promise 成功时,只有通过 then 方法指定回调函数才能执行。如果它失败了(例如由于网络错误),回调函数将不会执行。为了处理失败的 promise ,你可以通过 catch 附加另一个回调函数:

最后,为了测试的目的,我们可以使用 Promise.resolvePromise.reject 方法创建成功或失败的“虚拟” promises :

有关 promises 的更详细的教程,请查看本文

Link to this heading
Async 与 Await

当我们使用 promise 之后,我们只能通过then来传回回调函数(callback),而不能等待一个 Promise 执行完毕。鼓励开发者书写非阻塞的代码,尽管阻塞的代码写起来会比使用 promise 和回调函数容易。

然而,为了同步 promise, 我们需要允许 promise 之间相互等待。换句话说,如果一个异步的操作(例如封装在一个 promise 中)就应该去等待另一个异步的操作去完成。但是 JavaScript 解释器如何判断一个操作是否在 promise 中运行呢?

答案就是 async 关键字。每一个 async 函数都会返回一个 promise。也就是说, JavaScript 解释器就会把所有在 aysnc 函数中的操作封装到 promise 中并异步运行。这样就可以让它们去等待其他的 promise 完成。

按下 await 关键字,await 只能在 async 函数中使用,作用是让我们同步的等待另一个 promise 执行完毕。如果在 async 函数之外使用 promise 的话,依旧需要使用 then 回调函数:

现在,来看看如何解决刚在在上面一节出现的问题:

在上面的代码段中,我们将解决方案封装到了一个 async 函数中,这样我们就可以直接的等待(await) promise 执行完毕。这样避免了使用 then 回调函数。 最后,我们调用了 async 函数,这个函数只是简单的生成了一个封装调用其他 promise 的 promise。

在第一个例子(没有 asyncawait)中,那些 promise 会并行启动。这种情况下我们进行了同样的操作(第7,8行)。注意,直到11到12行,我们都没有使用 await。当所有的promise都执行完毕(resolve),我们才去阻塞程序的执行。之后,我们知道两个 promise 都执行完毕了(就像在之前的例子中,使用 Promise.all(...).then(...) 一样)。

在底层的计算过程上,这个过程和先前章节所述的过程是相同的,但是代码更加直观,可读性更好。

在引擎中,async/await 实际上转成了 promise 和 then传入的回调函数。换句话说,它是 promise 的语法糖。每次我们使用 await,解释器就会生成一个 promise,然后把其余的操作从 async 函数取出来放到 then 传入的回调函数中。

考虑一下下面的例子:

函数f在底层计算过程描述如下图。由于 f 是异步的,它将和调用者同步运行:

AsyncAwaitExample.png

函数 f 开始运行并且生成了一个 promise。同时,函数其余的部分被封装到回调函数中,安排在promise执行完毕之后再执行。

在前面几个例子中,我们假设 promise 成功的解决(reslove).于是,等待一个 promise 返回结果。事实上,如果等待的 promise 失败(reject)了,那么 async 函数将会返回一个异常(exception)。我们可以使用标准的 try/catch 去处理这种情况:

如果一个 async 函数没有处理异常,不管它是一个被拒绝(reject)的 promise 还是其他的 bug 造成的,它将返回一个被拒绝(reject)promise:

使用已知的异常处理机制将使我们方便的处理被拒绝(reject)的 promise.

Async/await 是 promises 的一种补充语言结构。它允许我们使用较少样板的 promise。然而 async/await不能取代纯粹 promise 的需要。例如,如果从一个普通的函数或者全局范围内调用一个 async 函数,我们无法使用 await,我们将借助于普通的promises(译者注:原文使用的是vanilla promise):

我通常尝试将我大部分的异步逻辑封装到一个或者几个 async 函数中,然后从非异步的代码中调用。这极大地减少了我编写then/catch回调的数量。

async/await 结构是更简洁处理 promise 的语法糖。每一个 async/await 结构都可以使用纯粹的 promise 重写。最终,这是一个风格和简洁方面的问题。

学者们指出并发(concurrency)和并行(parallelism)有区别。查看 Rob Pike 关于该主题或 我之前的帖子 。并发是关于组合独立进程(在过程的一般含义中)一起工作,而并行是关于实际上同时执行多个进程。并发是关于应用程序的设计和结构,而并行性就是实际的执行。

以多线程应用程序为例。将应用程序分隔为线程定义其并发模型。这些线程在可用内核上的映射定义了其级别或并行。并发系统可以在单个处理器上有效运行,在这种情况下,它不是并行的。

Concurrent vs. Parallel.png

在这种情况下,promise 允许我们将程序分解为可并行运行的并发模块。实际的 JavaScript 执行是否并行取决于 JavaScript 解释器实现。例如,node.js是单线程的,如果 promise 是 CPU 绑定的,那么并不会看到很多并行进程。然而,如果您通过类似 Nashorn 的工具将代码编译成 java 字节码,理论上你可以在不同的 CPU 核心上映射CPU绑定的 promise 并实现并行运行。因此,在我看来,promise(普通或通过async/await)构成了JavaScript应用程序的并发模型。

原文链接: Await and Async Explained with Diagrams and Examples