【译】Promises/A+ 规范

474 阅读7分钟

原文:Promises/A+

一个 Promise 代表一个异步操作最终结果。与 promise 进行交互的主要方式是通过 then 方法,then 方法是通过注册回调方法去接受一个 promise 的最终值或者没有完成的原因。

Promises/A+ 规范 详细说明了 then 方法的执行过程, 给所有符合 Promises/A+ 规范的 Promise 实现都提供了可以依赖的参照基础。因此这个规范是非常稳定的。尽管 Promises/A+ 组织可能会偶尔对该规范进行一些小的、向后兼容的改变,去处理一些新发现的极端情况。只有在进行了谨慎的考虑 讨论和测试之后,我们才会去进行大的或者不兼容的变更。

从历史上来说,Promises/A+ 把更早的 Promises/A 提案明确成了行为条款。一方面扩展了原规范 覆盖的实际行为,另一方面删减了原规范的一些没有详细说明或者有问题的部分。

最后,Promises/A+ 规范的核心并没有处理如何去创建,完成,或者拒绝一个 promise,而是聚焦于提供一个通用的 then 方法。上述的对于 promise 的操作方法在未来的其他规范中可能会提及。

1、术语

promise

promise 是一个拥有 then 方法的对象,并且其行为符合本规范;

thenable

是一个定义了 then 方法的对象或者函数(可以译为“拥有 then 方法”);

value

value 是任何一个合法的 JavaScript 值(包括 undefined,thenable 和 promise);

exception

异常,是 throw 抛出的一个值;

reason

表示一个 promise 被拒绝的原因;

2、要求

2.1、Promise 状态

一个 promise 必须是以下三种状态之一:pending,fulfilled, or rejected。

  • 当一个 promise 是 pending 状态时
    • 可以过渡到 fulfilled 状态或者 rejected 状态。
  • 当一个 promise 是 fulfilled 状态时
    • 不能过渡到任何状态
    • 必须拥有一个不可变的 value
  • 当一个 promise 是 rejected 状态
    • 不能过渡到任何状态
    • 必须拥有一个不可变的 reason

这里的不可变只是意味着恒等(可用===判断),而不是深层次的不可变。(比如 value 或者 reason 是一个 对象时,只要求引用地址相等,其属性值可以被更改)

2.1、Then 方法

一个 promise 必须提供一个 then 方法去接收它当前值, value 或者 reason。 一个 promise 的 then 方法接收两个参数:promise.then(onFulfilled,onRejected)

  • onFulfilledonRejected 都是可选参数:

    • 如果 onFulfilled 不是一个函数,会被忽略
    • 如果 onRejected 不是一个函数,会被忽略
  • 如果 onFulfilled 是一个函数:

    • 必须在 promise 过渡到 fulfilled 状态后调用,其第一个参数为 promise 的 value
    • 不能在过渡到 fulfilled 状态之前调用
    • 最多只能被调用一次
  • 如果 onRejected 是一个函数:

    • 必须在 promise 过渡到 rejected 状态后调用,其第一个参数为 promise 的 reason
    • 不能在 过渡到 rejected 状态之前调用
    • 最多只能被调用一次
  • 调用时机:onFulfilledonRejected 只有在执行环境堆栈仅包含平台代码时才可以被调用。[^1]

  • 调用要求:onFulfilledonRejected 只能以函数的形式被调用(没有 this 值)[^2]

  • 多次调用:then 方法可以被一个 promise 多次调用:

    • 当 promise 过渡到 fulfilled 状态时,所有的 onFulfilled 须按照其注册顺序依次执行
    • 当 promise 过渡到 rejected 状态时,所有的 onRejected 须按照其注册顺序依次执行
  • 返回:then 方法必须返回一个 promise。[^3]

     promise2 = promise1.then(onFulfilled, onRejected);
    
    • 如果 onFulfilled 或者 onRejected 返回一个值 x,则运行 Promise 解决程序 [[Resolve]](promise2, x)
    • 如果 onFulfilled 或者 onRejected 抛出一个异常 e,则 promise2必须过渡到 rejected 状态,并以 e 为 reason 参数
    • 如果 onFulfilled 不是一个函数,并且 promise1 已过渡到 fulfilled 状态,那么 promise2 必须也过渡到 fulfilled 状态并以 promise1的 value 作为 value
    • 如果 onRejected 不是一个函数,并且 promise 已拒绝执行,那么 promise2必须过渡到 rejected 状态并以 promise1 的 reason 作为 reason

2.2、Promise 的解决程序

promise 的解决程序是一个抽象的操作。它需要输入一个 promise 和 一个值,我们表示为 [[Resolve]](promise, x),如果 xthen 方法且看上去像 promise,解决程序会尝试让这个 promise 接收 x 的状态。否则就用x的值来执行。 这种 thenable 的特性使得 promise 的实现更具有通用性, 只要他暴露一个符合 Promises/A+ 规范的 then 方法。这使得那些符合 Promises/A+ 规范的实现可以与不符合规范但可用的实现可以良好的共存。

运行 [[Resolve]](promise, x) 需要执行以下步骤:

1、如果 promisex 指向同一个对象,以 TypeError 为 reason 拒绝执行 promise

2、如果 x 是 promise,则使 promise 接受 x 的状态:[^4]

  • 2.1、如果 x 处于 pending 状态,promise 需保持为等待状态直至 x 被执行或拒绝
  • 2.2、如果 x 处于 resolved 状态,用相同的 value 执行 promise
  • 2.3、如果 x 处于 rejected 状态,用相同的 reason 执行 promise

3、如果 x 是对象或者函数:

  • 3.1、把 x.then 赋值给 then[^5]
  • 3.2、如果取 x.then 的值时抛出错误 e,拒绝执行这个 promise 并以 e 作为 reason
  • 3.3、如果 then 是函数,将 x 作为函数的作用域 this 调用 then。传递两个回调函数作为参数,第一个参数叫 resolvePromise,第二个参数叫 rejectPromise
    • 3.3.1、如果 resolvePromisey 为 value 被调用,执行 [[Resolve]](promise, y)
    • 3.3.2、如果 rejectPromiser 为 reason 被调用, 拒绝 promise 并以 r 为 reason
    • 3.3.3、如果 resolvePromiserejectPromise 都被调用,或者被同样的参数调用了多次,则优先采用首次调用,并忽略其他的调用
    • 3.3.4、如果调用 then 方法抛出了异常 e:
      • 如果 resolvePromise 或者 rejectPromise 已经被调用,忽略异常
      • 如果没有被调用,拒绝执行 promise 并以 e 为 reason
  • 3.4、如果 then 不是一个函数,成功执行 promise 并以 x 为 value

4、如果 x 不是一个对象或者函数, 成功执行 promise 并以 x 为 value

如果一个 promise 被一个循环的 thenable 链中的对象成功执行,[[Resolve]](promise, thenable) 的递归性质又使得其再次调用,根据上述的算法将会陷入无限递归之中。算法虽然没有要求,但也鼓励实现者去检测这样的递归。如果检测到,则以一个可识别的 TypeError 为 reason 来决绝执行 promise。[^6]

3、注释

[^1]:这里的【平台代码】指的是引擎、环境以及 promise 的实现代码。实践中,这个要求确保了 onFulfilledonRejected 异步执行,并且是在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。这个可以使用像 setTimeout 或者 setImmediate 这样的“宏任务”机制实现,也可以用像 MutationObserver 或者 process.nextTick 这样的“微任务”机制 来实现。由于 promise 的实现本身就是平台代码(JavaScript),所以它本身在处理程序时可能已经包含了一个任务调度队列

[^2]: 在严格模式下,函数中this 会是 undefined。在宽松模式下,this 会是全局对象。

[^3]: 代码实现在满足所有要求的情况下,允许 promise2===promise1。每一个实现都要文档说明是否允许 promise2===promise1 以及如果允许的话,是在什么情况下允许。

[^4]: 通常,如果 x 符合当前实现,我们才认为是真正的 promise。这一规则允许那些特例实现可以接受符合已知规范的 promise 的状态。

[^5]: 这里我们先存储了 x.then 的引用,然后测试并后调用这个引用,避免多次取用 x.then 属性。这种预防措施确保了该属性的一致性,因为其值可能在检索调用时改变。

[^6]: 实现不应该对 thenable 链的深度设限,并假定超出设定限度的递归就是无限循环。只有真正的循环递归才会导致 TypeError 异常; 如果一条无限长的链上 thenable 均不同,那么递归下去永远是正确的行为。