37.异步编程的 Promise

原文: http://exploringjs.com/impatient-js/ch_promises.html

在本章中,我们将探索 Promises,这是另一种提供异步结果的模式。

本章以的前一章为基础,背景介绍了 JavaScript 中的异步编程。

37.1。使用 Promises 的基础知识

Promise 是一种提供异步结果的模式。

37.1.1。使用基于 Promise 的功能

以下代码是使用基于 Promise 的函数addAsync()的示例(其实现很快就会显示):

Promise 结合了回调模式事件模式的各个方面:

  • 与回调模式一样,Promises 专注于提供一次性结果。

  • 与某些事件模式类似,基于 Promise 的函数通过返回一个对象( Promise )来传递其结果。使用该对象,可以注册处理结果(.then())和错误(.catch())的回调。

  • Promise 的一个独特之处在于你可以链.then().catch(),因为它们都返回 Promise。这有助于顺序调用多个异步函数。我们稍后会详细介绍。

37.1.2。什么是 Promise?

那么什么是 Promise?有两种方式可以看待它:

  • 一方面,它是最终结果的占位符或容器,最终将被传递。
  • 另一方面,它是一个可以注册监听器的对象。

37.1.3。实现基于 Promise 的功能

这是你如何实现一个基于 Promise 的函数,它增加了两个数字xy

使用这种返回 Promise 的方式,addAsync()立即调用Promise构造函数。该函数的实际实现驻留在传递给该构造函数的回调中(行 A)。该回调具有两个功能:

  • resolve用于传递结果(如果成功)。
  • reject用于传递错误(如果发生故障)。

37.1.4。Promise 的状态

Figure 21: A Promise can be in either one of three states: pending, fulfilled, or rejected. If a Promise is in a final (non-pending) state, it is called settled.

Figure 21: A Promise can be in either one of three states: pending, fulfilled, or rejected. If a Promise is in a final (non-pending) state, it is called settled.

21 描述了 Promise 可以进入的三种状态.Devises 专注于一次性结果并保护您免受 竞争条件 (过早注册或太晚注册):

  • 如果您过早注册.then()回调或.catch()回调,则会在 Promise 结算后通知。
  • 一旦确定了 Promise,就会缓存结算值(结果或错误)。因此,如果在结算后调用.then().catch(),它们将收到 chaed 值。

此外,一旦 Promise 得到解决,其状态和结算值就不能再改变了。这有助于使代码可预测并强制实施 Promises 的一次性特性。

接下来,我们将看到更多创建 Promise 的方法。

37.1.5。 Promise.resolve():创建一个满足给定值的 Promise

Promise.resolve(x)创建一个使用值x实现的 Promise:

如果参数已经是 Promise,则返回不变:

因此,给定任意值x,您可以使用Promise.resolve(x)确保您拥有 Promise。

请注意,名称是resolve,而不是fulfill,因为如果参数是被拒绝的 Promise,.resolve()会返回被拒绝的 Promise。

37.1.6。 Promise.reject():创建一个被给定值拒绝的 Promise

Promise.reject(err)创建一个使用值err实现的 Promise:

37.1.7。返回并投入.then()回调

.then()处理 Promise 履行。它返回一个新鲜的 Promise。如何解决 Promise 取决于回调内部发生了什么。我们来看看三种常见情况。

37.1.7.1。返回非 Promise 值

首先,回调可以返回非 Promise 值(行 A)。因此,.then()返回的 Promise 满足该值(如 B 行所示):

37.1.7.2。回报 Promise

其次,回调可以返回 Promise p(行 A)。因此,p“成为”.then()返回的内容(.then()已经返回的 Promise 实际上被p替换)。

37.1.7.3。抛出异常

第三,回调可以抛出异常。因此,.then()返回的 Promise 被该异常拒绝。也就是说,同步错误被转换为异步错误。

37.1.8。 .catch()及其回调

.then().catch()之间的唯一区别是后者是由拒绝而不是履行引发的。但是,这两种方法都以相同的方式将其回调的操作转换为 Promises。例如,在以下代码中,行 A 中.catch()回调返回的值将成为履行值:

37.1.9。链接方法调用

由于.then().catch()总是返回 Promises,您可以创建任意长方法调用链:

在某种程度上,.then()是同步分号的异步版本:

  • .then()顺序执行两个异步操作。
  • 分号按顺序执行两个同步操作。

您还可以将.catch()添加到混合中,并让它同时处理多个错误源:

37.1.10。Promise 的好处

在处理一次性结果时,这些是 Promise 优于普通回调的一些优点:

  • 基于 Promise 的函数和方法的类型签名更清晰:如果函数是基于回调的,则某些参数是关于输入的,而最后的一个或两个回调是关于输出的。使用 Promises,与输出相关的所有内容都通过返回值处理。

  • 链接异步处理步骤更方便。

  • 错误处理负责同步和异步错误。

  • 编写基于 Promise 的函数稍微容易一些,因为您可以使用一些调用函数和处理结果的同步工具(例如.map())。我们将在本章末尾看到一个例子。

  • Promise 是一个单一的标准,正逐渐取代几个互不相容的替代品。例如,在 Node.js 中,许多函数现在都可以在基于 Promise 的版本中使用。新的异步浏览器 API 通常是基于 Promise 的。

Promises 最大的优势之一就是不直接使用它们:它们是 异步函数 的基础,HTG1 是一种用于执行异步计算的同步语法。异步函数将在下一章中介绍。

37.2。例子

看到他们的行动有助于理解 Promises。我们来看看例子。

37.2.1。 Node.js:异步读取文件

考虑以下带有 JSON 数据的文本文件person.json

让我们看看两个版本的代码,它们读取这个文件并将其解析为一个对象。首先,基于回调的版本。第二,基于 Promise 的版本。

37.2.1.1。基于回调的版本

以下代码读取此文件的内容并将其转换为 JavaScript 对象。它基于 Node.js 样式的回调:

fs是用于文件系统操作的内置 Node.js 模块。我们使用基于回调的函数fs.readFile()来读取名称为person.json的文件。如果我们成功,内容将通过参数text作为字符串传递。在 C 行中,我们将该字符串从基于文本的数据格式 JSON 转换为 JavaScript 对象。 JSON是 JavaScript 标准库的一部分。

请注意,有两种错误处理机制:行 A 中的if负责fs.readFile()报告的异步错误,而行 B 中的try负责JSON.parse()报告的同步错误。

37.2.1.2。基于 Promise 的版本

以下代码使用readFileAsync(),一个基于 Promise 的fs.readFile()版本(通过util.promisify()创建,稍后会解释):

函数readFileAsync()返回 Promise。在 行 A 中,我们通过 Promise 的方法.then()指定成功回调。 then回调中的剩余代码是同步的。

.then()返回一个 Promise,它允许在 B 行调用 Promise 方法.catch()。我们用它来指定一个失败的回调。

请注意,.catch()允许我们处理readFileAsync()的异步错误和JSON.parse()的同步错误。我们稍后会看到它究竟是如何工作的。

37.2.2。浏览器:Promisifying XMLHttpRequest

我们以前见过基于事件的XMLHttpRequest API,用于在 Web 浏览器中下载数据。以下函数宣传 API:

注意如何通过resolve()reject()处理XMLHttpRequest的结果:

  • 成功的结果导致返回的 Promise 得以实现(行 A)。
  • 错误导致 Promise 被拒绝(B 行和 C 行)。

这是您使用httpGet()的方式:

37.异步编程的 Promise - 图2 练习:找出 Promise

exercises/promises/promise_timeout_test.js

37.2.3。 Node.js:util.promisify()

util.promisify()是一个实用程序函数,它将基于回调的函数f转换为基于 Promise 的函数f。也就是说,我们将从这种类型的签名:

  1. f(arg_1, ···, arg_n, (err: Error, result: T) => void) : void

对于这种类型的签名:

  1. f(arg_1, ···, arg_n) : Promise<T>

以下代码统一了基于回调的fs.readFile()(行 A)并使用它:

37.异步编程的 Promise - 图3 练习:util.promisify()

  • 使用util.promisify()exercises/promises/read_file_async_exrc.js
  • 自己实施util.promisify()exercises/promises/my_promisify_test.js

37.2.4。浏览器:获取 API

所有现代浏览器都支持 Fetch,这是一种基于 Promise 的新 API,用于下载数据。可以把它想象成XMLHttpRequest的基于 Promise 的版本。以下是 API 的摘录:

这意味着,你可以使用fetch()如下:

37.异步编程的 Promise - 图4 练习:使用 fetch API

exercises/promises/fetch_json_test.js

37.3。错误处理:不要混合拒绝和异常

异步代码中错误处理的一般规则是:

不要混合(异步)拒绝和(同步)异常

理由是,如果您可以使用单一的错误处理机制,那么您的代码就不那么冗余了。

唉,很容易意外地打破这个规则。例如:

问题是,如果在 行 A 抛出异常,那么asyncFunc()将抛出异常。该函数的调用者只会期待拒绝,并且不会为异常做好准备。我们可以通过三种方式解决此问题。

我们可以在try-catch语句中包装函数的整个主体,并在抛出异常时返回被拒绝的 Promise:

鉴于.then()将异常转换为拒绝,我们可以在.then()回调中执行doSomethingSync()。为此,我们通过Promise.resolve()启动 Promise 链。我们忽略了最初的 Promise 的履行值undefined

最后,new Promise()还将异常转换为拒绝。因此,使用此构造函数与以前的解决方案类似:

37.4。基于 Promise 的函数同步启动,异步解决

大多数基于 Promise 的函数执行如下:

  • 他们的执行立即开始,同步。
  • 但他们返回的 Promise 保证可以异步结算(如果有的话)。

以下代码演示了:

我们可以看到new Promise()的回调在代码结束之前执行,而结果在稍后传递(行 A)。

这意味着您的代码可以依赖于运行到完成语义(如前一章中所述),并且链接 Promise 不会使处理时间的其他任务匮乏。

此外,此规则导致基于 Promise 的函数始终异步返回结果。有时不是立即,有时是异步的。这种可预测性使代码更易于使用。有关更多信息,请参阅 Isaac Z. Schlueter 的“异步设计 API”

37.5。 Promise.all():并发和 Promise 数组

37.5.1。顺序执行与并发执行

请考虑以下代码:

使用.then(),始终执行基于 Promise 的功能:仅在asyncFunc1()的结果确定后,才会执行asyncFunc2()

相反,辅助函数Promise.all()以更多并发的方式执行基于 Promise 的函数:

它的类型签名是:

参数promises是 Promises 的可迭代参数。结果是单个 Promise,其结算方式如下:

  • 如果满足所有输入 Promise,则输出 Promise 将通过一个履行值数组来实现。
  • 如果至少有一个输入 Promise 被拒绝,则输出 Promise 将被拒绝,并带有输入 Promise 的拒绝值。

换句话说:你从一个可变的 Promises 转到一个 Array 的 Promise。

37.5.2。并发提示:关注计算何时开始

确定“并发”异步代码的方法的提示:关注异步计算何时开始,而不是如何处理 Promise。例如,以下使用.then()的代码与使用Promise.all()的版本一样“并发”:

asyncFunc1()asyncFunc2()大致同时开始。一旦两个 Promise 都满足,两个.then()调用几乎立即执行。如果首先满足promise1,这种方法甚至比使用Promise.all()(等待所有 Promise 都满足)更快。

37.5.3。 Promise.all()是 fork-join

Promise.all()与并发模式“fork join”松散相关。例如:

37.5.4。通过Promise.all()异步.map()

诸如.map().filter()等的数组变换方法用于同步计算。例如:

.map()的回调是否可能是基于 Promise 的函数?是的,如果你使用Promise.all()将一个 Promises 数组转换为一个(履行)值数组:

37.5.4.1。一个更现实的例子

下面的代码是一个更现实的例子:我们使用.map()将示例从 fork-join 部分转换为一个函数,其参数是一个带有要下载的文本文件 URL 的 Array。

37.异步编程的 Promise - 图5 练习:Promise.all()和列表文件

exercises/promises/list_files_async_test.js

37.6。链接 Promise 的提示

本节提供了链接 Promise 的提示。

37.6.1。链接错误:失去尾巴

问题:

计算从asyncFunc()返回的 Promise 开始。但之后,计算继续,并通过.then()创建另一个 Promise。 foo()返回前 Promise,但应返回后者。这是如何解决它:

37.6.2。链接错误:嵌套

问题:

行 A 中的.then()是嵌套的。扁平结构会更好:

37.6.3。链接错误:比必要的嵌套更多

这是可避免嵌套的另一个例子:

我们可以再次获得扁平结构:

37.6.4。嵌套本身并不邪恶

在以下代码中,我们受益于 而不是 嵌套:

我们在 行 A 接收异步结果。在 B 行中,我们正在嵌套,因此我们可以在回调内和 C 行中访问变量connection

37.6.5。链接错误:创建 Promise 而不是链接

问题:

在 行 A 中,我们创建了一个 Promise 来提供db.insert()的结果。这是不必要的冗长,可以简化:

关键的想法是我们不需要创造一个 Promise;我们可以返回.then()调用的结果。另一个好处是我们不需要捕获并重新拒绝db.insert()的失败。我们只是将其拒绝传递给.insertInto()的来电者。

37.7。进一步阅读