二、承诺 API 及其兼容性

承诺对于 JavaScript 世界来说是相当新的,但变通方法已经存在了一段时间。正如我们在前一章中所看到的,可以通过事件或回调来解决 JavaScript 中的异步编程问题。您还了解了为什么承诺与传统的技术不同。

接下来,我们将详细介绍 Promise API。您还将了解当前对 promises 标准的浏览器支持,并查看实现 promises 和 promise-like 特性的 JavaScript 库。在本章中,我们将介绍以下主题:

  • Promise API 及其详细信息
  • 浏览器兼容性
  • 承诺实现
  • 具有类似承诺功能的库

了解 API

在整个这本书中,我们将主要讨论和使用承诺规范/A+(中定义的承诺 http://promisesaplus.com/ 。Promises/A+组织制定了 Promises/A+规范,目的是将最初的 Promises/A 规范阐释为更清晰、测试更好的规范。以下是他们网站上的引文:

|   | 承诺/A+基于 CommonJS 承诺/A 提案中提出的概念和thenAPI。 |   | |   | --http://promisesaplus.com/differences-from-promises-a |

这些差异体现在三个层面:遗漏、补充和澄清。在遗漏层面,Promises/A+已从原始特征中删除了以下特征:

  • 进度处理:此功能包括一个回调函数,在操作/承诺仍在进行中时处理该操作/承诺,如未完成或拒绝。它被删除是因为实现者认为,在实践中,这些功能被证明是未被指定的,并且目前在 promise 实现者社区中对它们的行为没有完全一致的意见。
  • 交互承诺:此功能是之前承诺/A 提案中的扩展承诺,基本支持承诺方式的两个新增功能;get(propertyName),从本承诺的目标请求给定属性;call(functionName, arg1, arg2, ...)在承诺的目标上调用其参数中的给定方法/函数。在新的 A+规范中,当实现互操作承诺所需的基本 API 时,此功能以及两个函数callget被认为超出了范围。
  • promise!== resultPromise:这是旧提案中的一项要求,其中规定承诺的结果不应等于承诺,例如var resultPromise = promise.then(onFulfilled, onRejected)。事实上,任何实现都可以允许resultPromise === promise,只要实现满足所有要求。

在补充层面,承诺/A+规范在现有承诺/A 提案中增加了以下特征和要求:

  • onFulfilledonRejected返回 thenable 的场景中的行为规范,包括解析过程的详细信息。
  • 原因传递给onRejected处理程序,该处理程序必须是在这种情况下抛出的异常。
  • 必须异步调用的两个处理程序onFulfilledonRejected
  • 必须调用函数的两个处理程序onFulfilledonRejected
  • Implementations must abide by the exact ordering of calls to the handlers onFulfilled and onRejected in case of consequent calls to the then method on the same promise. In a more spoken language, this means that, if the then method is called more than once on the same promise as in promise.then().then(), all the onFulfilled handlers used in these then calls must execute in the order of the originating calls to then. Hence, the onFulfilled callback in the first then function will execute first, followed by the onFulfilled callback in the second then, and so on. The same thing applies to the execution of the onRejected callbacks in such a scenario. Was it very complex? Maybe the following example can explain it better:

    js var p = [[promise]]; p.then(); p.then();

    前面的代码与下面的代码行不同:

    js promise.then().then();

    不同之处在于promise.then()可能会返回不同的承诺。

最后,在级别的澄清中,Promises/A+提案应用了与 Promises/A 不同的命名,因为新规范的作者希望反映在 promise 实现中传播的词汇表。这些变化包括:

  • 承诺状态被称为未决、履行和拒绝,取代未履行、履行和失败
  • 承诺履行时,该承诺具有;同样,当承诺被拒绝时,它有一个原因

then方法是 API 中的主要参与者。如前一章所述,如果一个对象没有指定用于检索和访问其当前或最终值或原因的then方法,则该对象不被视为承诺。此方法接受两个需要作为函数的参数,如下例所示:

promise.then(onFulfilled, onRejected);

考虑到前面一个简单的then方法的代码示例,让我们深入了解then及其参数的细节:

  • 参数onFulfilledonRejected都是可选的。
  • 两个参数都必须是函数;否则,它必须被忽略。
  • 在同一个then调用中,两个参数的调用不得超过一次。
  • onFulfilled参数必须仅在承诺履行后调用,并将承诺的值作为其第一个参数。
  • onRejected参数必须在承诺被拒绝后调用,第一个参数为承诺被拒绝的原因。
  • onFulfilledonRejected参数不能作为this值传递,因为如果我们对 JavaScript 代码应用严格模式,这将在处理程序中被视为未定义;在 quirks 模式下,它将被视为该 JavaScript 代码中的全局对象。
  • then方法可以在同一承诺上调用多次。
  • 当一个承诺被履行时,所有相应的onFulfilled处理程序必须按照其对then的原始调用的相同顺序执行。同样的规则也适用于onRejected回调。
  • then方法必须返回如下承诺:

    js promiseReturned = promise.then(onFulfilled, onRejected);

  • 如果onFulfilledonRejected返回值x,则必须调用承诺解析过程解析值x,如下代码所示:

    js promiseReturned = promise1.then(onFulfilled, onRejected); [[Resolve]](promiseReturned, x).

  • 如果onFulfilledonRejected处理程序抛出异常e,则必须以e作为拒绝或失败的原因来拒绝promiseReturned

  • 如果onFulfilled不是一个函数,并且承诺已实现,则必须使用相同的值来实现promiseReturned
  • 如果onRejected不是函数且promise1被拒绝,则必须以相同的原因拒绝promiseReturned

前面的列表是 Promises/a+开放标准中定义和规定的 Promises 和then方法的详细规范。我们在前面的列表中讨论了承诺解决程序,但我们还不知道它是什么。承诺解析过程基本上是一个抽象操作,以承诺和值为参数,如下所示:

[[Resolve]](promise, x)]

如果x是一个 thenable,意味着它是一个定义then方法的对象或函数,resolve方法将尝试强制承诺假设为x的状态,前提是x的行为至少有点像承诺。否则将以x的价值履行承诺。

promise 解析过程用于处理表的技术允许 promise 实现彼此可靠地工作,只要该 promise 公开符合 Promises/a+的then方法。此外,它还允许实现将非标准实现与合理的then方法进行集成。

|   | 承诺解决程序(PRP)不是公共 API。它旨在描述一个重要但抽象的内部/私有过程,这里的“过程”只是指“算法”,而不是具体的 JavaScript 函数。一个特定的承诺实现可能会以他们认为最好的方式实现它。 |   | |   | --Promises/A+联合编辑 Brian Cavalier 解释 |

承诺解决程序允许我们正确执行promise.resolve。还必须保证then的正确实施。您可能会注意到,promise 解析过程没有返回值,因为它是一个抽象过程,可以以特定 promise 实现的作者认为合适的任何方式实现。因此,只要实现最终目标,返回值就留给实现者,即将承诺置于与x相同的状态。因此,从概念上讲,它影响承诺的状态转换。

虽然 promise resolution procedure 算法的实现由实现者来完成,但它有一些自己的规则,如果我们想在需要运行它时遵守建议,我们应该遵守这些规则。这些规则如下:

  1. 如果一个承诺和x引用了同一个对象,则该承诺应该被拒绝,并以TypeError作为onRejected处理程序的原因。
  2. 如果x是一个承诺,我们应该接受它的当前状态。该规则允许使用特定于实现的行为来实际采用已知符合承诺的状态。以下是条件:
    • 如果x处于未决状态,则承诺必须保持未决状态,直到x履行或拒绝
    • 如果x履行时,承诺应以x具有的相同值履行
    • 如果x被拒绝,该承诺应被拒绝,理由与x被拒绝的原因相同
  3. 如果x是一个对象或函数,而不是承诺,则执行以下操作:
    • 当我们想调用then时,方法应该是x.then。这是一项防御性措施,必须确保在accessor财产面前的一致性。它的值在我们检索时可能会更改。
    • 如果检索到x.then属性最终抛出异常e,则应以e为理由拒绝承诺。
    • 如果then是一个函数,用xthis的值调用。第一个参数应该是resolvePromise,第二个参数应该是rejectPromise
    • 如果then不符合功能要求,则直接使用x履行承诺。
  4. 如果x既不是对象也不是功能,则应使用x履行承诺。

让我们看一看第三条规则。我们看到,如果then是函数,那么第一个参数应该是resolvePromise,第二个参数应该是rejectPromise,其中适用以下规则:

  1. 如果/当使用值z调用resolvePromise时,实现必须运行[[Resolve]](promise, z)
  2. 如果/当调用rejectPromise时有j的原因,则实现必须以j 的原因拒绝承诺。
  3. 如果处理程序resolvePromiserejectPromise都被调用,或者在对同一参数进行多个调用的情况下,第一个调用应优先,并且忽略任何其他后续调用。
  4. 如果调用导致抛出异常 e,则有两个条件:
    • 如果已经调用了resolvePromiserejectPromise处理程序,我们应该忽略then
    • 否则,执行应拒绝承诺,并返回e作为理由

前面的一长串规则为实施者提供了指导。所以,如果您在公共 API 中实现then,这些规则应该应用于您的算法,以符合 Promises/A+标准规范。我向 Brian Cavalier 询问了 PRP 的需求,他补充道:

PRP 最重要的一个方面是,它经过精心设计,允许不同的 promise 实现以可靠的方式进行互操作。

此外,承诺解决程序甚至允许在不符合要求(且有点危险)的情况下实现正确性。例如,使用resolve函数将不符合 a+标准的 jQuery 版本的承诺转换为真正简单的符合标准的承诺。以下代码说明了该实现:

// an ajax call that returns jquery promisevar jQueryPromise = $.ajax('/sample.json'); //correct it and convert it to a standard conforming promisevar standardPromise = Promise.resolve(jQueryPromise); 

最终,promise/A+的核心目标是提供尽可能最小、最简单的规范,允许不同 promise 实现的可靠互操作,即使面临危险。

为了消除可能出现的任何混淆,promise 解析过程与某些实现在其公共 API 中提供的promise.resolve方法并不完全相同。

为了与 Promises/A+标准的核心目标保持一致,Promises/A+组织创建了一个符合性测试套件,以测试 promise 库或 API 实现是否符合 Promises/A+规范。符合性测试,可在找到 https://github.com/promises-aplus/promises-tests ,通过测试then检查承诺解决程序的正确性。这些测试还旨在为实现是否符合要求和标准提供更具体的指导和证据。

浏览器支持和兼容性

JavaScript 与浏览器紧密结合,这同样适用于承诺,因为承诺在以前版本的 ECMAScript 中不是标准,并且将成为新的 ECMAScript 6 版本的一部分;并非所有浏览器都支持它们。此外,承诺是可以实现的,我们将看到几个库提供类似承诺的特性或公开承诺功能。在本章的剩余部分中,我们将介绍这两点,这两点在涉及承诺时至关重要。

检查浏览器兼容性

与任何客户端技术一样,JavaScript 已明确开发用于 web 浏览器和 HTML 页面。它使用浏览器来完成这项工作,这就是为什么它是一种脚本语言。一旦脚本被发送到浏览器,就由浏览器来处理它。那里有严重的依赖性;因此,浏览器兼容性至关重要。

一些浏览器已经实现了承诺;在编写本书时,有一小部分浏览器支持 promises,如下 Kangax 的 ECMAScript 6 兼容性表所示:

Checking the browser compatibility

来源:http://kangax.github.io/compat-table/es6/#Promise

兼容性表中使用的缩写词

IE 代表 Internet Explorer,FF 代表 Firefox,CH 代表 Chrome,SF 代表 Safari,WK 代表 Webkit,OP 代表 Opera。

正如我们在上表中看到的,默认情况下,只有最新的三个版本的 Firefox(从 29 版开始)和 Chrome(从 32 版开始)才启用承诺。不用担心,因为有一个 polyfill 可以为还不支持它的浏览器添加承诺功能。

polyfill 是雷米·夏普(Remy Sharp)创造的一个相当新的术语,在网络开发者社区中越来越流行。它代表了一段代码,提供了我们期望浏览器本机提供的技术和行为。我们可以把它看作是计算方面的补丁。

此 polyfill 具有魔力,并为我们提供承诺支持,可从以下链接下载:https://www.promisejs.org/polyfills/promise-4.0.0.js 。它基本上增加了对尚未在本地实现的浏览器的承诺的支持。它还可以用于支持 Node.js 中的承诺。以下代码示例显示了如何将其包含在我们的代码文件中:

<script src="https://www.promisejs.org/polyfills/promise-4.0.0.js"></script>

我们之所以显示 ECMAScript 6 兼容性表,是因为承诺是 ECMAScript 6 规范的一部分,它提供承诺作为一级语言特性,并且实现是基于承诺/a+方案的。

具有承诺式功能的图书馆

对于 web 开发和 JavaScript 的世界来说,承诺的概念并不新鲜。开发人员可能通过库以非标准化的方式满足或使用了 JavaScript 中的承诺。这些库是 promise 概念的实现;其中一些是遵循规范的实现,并开始采用承诺模式,而许多则不是。此外,其中一些库不符合 Promises/A+标准,这是在我们的项目中选择要使用的 JavaScript 库时的一个非常重要的要求。

提示

开发人员可以使用 Compliance 测试套件测试其实现承诺的库和 API 是否符合承诺/A+标准。

以下是一些完全符合 Promises/a+规范的库的列表,因此我可以毫无保留地推荐:

  • Q.js:由 Kris Kowal 和 Domenic Denicola 开发,它包含一个功能齐全的 promise 库,其中包括 Node.js 的适配器和对进度处理程序的支持。可从下载 https://github.com/kriskowal/q
  • RSVP.js:由叶胡达·卡茨开发,其特点是非常小巧、轻巧的 promise 库。可从下载 https://github.com/tildeio/rsvp.js
  • when.js:由 Brian Cavalier 开发,它提供了一个中间库,包括管理最终操作集合的功能。它还具有公开承诺的进度和取消处理程序的功能。可从下载 https://github.com/cujojs/when

此外,我们还有thenhttps://github.com/then )这是一组库是简单的承诺/a+实现,满足规范,并在承诺履行或拒绝时通过一些功能(如进度)对其进行扩展。

此外,著名的 jQuery 有一个 API,他们称之为 Deferred-可在上找到 http://api.jquery.com/jquery.deferred/ ,声称类似于承诺。jQuery 的延迟没有返回then的新承诺,因为规范需要,直到版本 1.8;因此,依赖 jQuery 的开发人员没有获得 promises 模式的全部功能。此外,使用此实现编写的许多代码与实际遵守规范的其他 promise 实现并不完美。延迟不符合 Promise/A+,至少在规范的第二部分中是这样的,该部分规定then在执行一个处理程序时不会返回新的 Promise 对象。因此,我们不能有then函数的函数组合和链接,最终也不能有由于断链导致的错误冒泡,这是规范中最重要的两点。这使得 jQuery 与众不同,而且有些不太有用。尽管如此,如果我们需要使用 jQuery 公开的promise对象或任何其他不符合该问题规范的库,我们可以使用前面列出的库之一将不符合要求的承诺转换为与 a+提案相关的真实承诺。例如,使用 Q,我们可以使用以下代码将 jQuery 承诺转换为标准承诺:

var SpecPromise = Q.when($.get("http://example.com/json"));

另一个示例将使用 Promise polyfill 库(https://www.promisejs.org/polyfills/promise-4.0.0.js )如下代码所示:

var specPromise = Promise.resolve($.ajax(' http://example.com/json););

尽管这些 promise 实现遵循标准化行为,但它们的总体 API 有所不同。

总结

正如我们所看到的,承诺的概念并不是很新,在 JavaScript 中已经存在,通过库实现了不同的实现,无论是标准的还是其他的。然而,现在,所有这些努力都已在大多数库都遵守的 Promises/A+社区规范中得到了总结。因此,我们现在通过一个标准的Promise类在 JavaScript 中对承诺提供了本机支持,该类包含在下一版本的 ECMAScript ECMAScript 6 中,允许 web 平台 API 为其异步操作返回承诺。此外,我们还深入介绍了 promise API 和then方法,并了解了新标准的当前浏览器兼容性。最后,我们简要介绍了一些实现承诺并符合承诺/A+规范的库

在下一章中,我们将讨论承诺的链接,以及如何使用then方法来实现多个异步操作。