【翻譯】Promises/A+規範

於明昊發表於2014-07-27

英文原文:Promise/A+

我的部落格:Promise A+ 規範

譯者序:一年前曾譯過 Promise/A+ 規範,適時完全不懂 Promise 的思想,純粹將翻譯的過程當作學習,舊文譯下來詰屈聱牙,讀起來十分不順暢。誰知這樣一篇拙譯,一年之間竟然點選數千,成為谷歌搜尋的頭條。今日在理解之後重譯此規範,以饗讀者。

一個開放、健全且通用的 JavaScript Promise 標準。由開發者制定,供開發者參考。


譯文術語

  • 解決(fulfill):指一個 promise 成功時進行的一系列操作,如狀態的改變、回撥的執行。雖然規範中用 fulfill 來表示解決,但在後世的 promise 實現多以 resolve 來指代之。
  • 拒絕(reject):指一個 promise 失敗時進行的一系列操作。
  • 終值(eventual value):所謂終值,指的是 promise 被解決時傳遞給解決回撥的值,由於 promise 有一次性的特徵,因此當這個值被傳遞時,標誌著 promise 等待態的結束,故稱之終值,有時也直接簡稱為值(value)。
  • 據因(reason):也就是拒絕原因,指在 promise 被拒絕時傳遞給拒絕回撥的值。

Promise 表示一個非同步操作的最終結果,與之進行互動的方式主要是 then 方法,該方法註冊了兩個回撥函式,用於接收 promise 的終值或本 promise 不能執行的原因。

本規範詳細列出了 then 方法的執行過程,所有遵循 Promises/A+ 規範實現的 promise 均可以本標準作為參照基礎來實施 then 方法。因而本規範是十分穩定的。儘管 Promise/A+ 組織有時可能會修訂本規範,但主要是為了處理一些特殊的邊界情況,且這些改動都是微小且向下相容的。如果我們要進行大規模不相容的更新,我們一定會在事先進行謹慎地考慮、詳盡的探討和嚴格的測試。

從歷史上說,本規範實際上是把之前 Promise/A 規範 中的建議明確成為了行為標準:我們一方面擴充套件了原有規範約定俗成的行為,一方面刪減了原規範的一些特例情況和有問題的部分。

最後,核心的 Promises/A+ 規範不設計如何建立、解決和拒絕 promise,而是專注於提供一個通用的 then 方法。上述對於 promises 的操作方法將來在其他規範中可能會提及。

術語


Promise

promise 是一個擁有 then 方法的物件或函式,其行為符合本規範;

thenable

是一個定義了 then 方法的物件或函式,文中譯作“擁有 then 方法”;

值(value)

指任何 JavaScript 的合法值(包括 undefined , thenable 和 promise);

異常(exception)

是使用 throw 語句丟擲的一個值。

據因(reason)

表示一個 promise 的拒絕原因。

要求


Promise 的狀態

一個 Promise 的當前狀態必須為以下三種狀態中的一種:等待態(Pending)執行態(Fulfilled)拒絕態(Rejected)

等待態(Pending)

處於等待態時,promise 需滿足以下條件:

  • 可以遷移至執行態或拒絕態

執行態(Fulfilled)

處於執行態時,promise 需滿足以下條件:

  • 不能遷移至其他任何狀態
  • 必須擁有一個不可變的終值

拒絕態(Rejected)

處於拒絕態時,promise 需滿足以下條件:

  • 不能遷移至其他任何狀態
  • 必須擁有一個不可變的據因

這裡的不可變指的是恆等(即可用 === 判斷相等),而不是意味著更深層次的不可變(譯者注:蓋指當 value 或 reason 不是基本值時,只要求其引用地址相等,但屬性值可被更改)。

Then 方法

一個 promise 必須提供一個 then 方法以訪問其當前值、終值和據因。

promise 的 then 方法接受兩個引數:

promise.then(onFulfilled, onRejected)

引數可選

onFulfilledonRejected 都是可選引數。

  • 如果 onFulfilled 不是函式,其必須被忽略
  • 如果 onRejected 不是函式,其必須被忽略

onFulfilled 特性

如果 onFulfilled 是函式:

  • promise 執行結束後其必須被呼叫,其第一個引數為 promise 的終值
  • promise 執行結束前其不可被呼叫
  • 其呼叫次數不可超過一次

onRejected 特性

如果 onRejected 是函式:

  • promise 被拒絕執行後其必須被呼叫,其第一個引數為 promise 的據因
  • promise 被拒絕執行前其不可被呼叫
  • 其呼叫次數不可超過一次

呼叫時機

onFulfilledonRejected 只有在執行環境堆疊僅包含平臺程式碼時才可被呼叫 注1

呼叫要求

onFulfilledonRejected 必須被作為函式呼叫(即沒有 this 值)注2

多次呼叫

then 方法可以被同一個 promise 呼叫多次

  • promise 成功執行時,所有 onFulfilled 需按照其註冊順序依次回撥
  • promise 被拒絕執行時,所有的 onRejected 需按照其註冊順序依次回撥

返回

then 方法必須返回一個 promise 物件 注3

promise2 = promise1.then(onFulfilled, onRejected);   
  • 如果 onFulfilled 或者 onRejected 返回一個值 x ,則執行下面的 Promise 解決過程[[Resolve]](promise2, x)
  • 如果 onFulfilled 或者 onRejected 丟擲一個異常 e ,則 promise2 必須拒絕執行,並返回拒因 e
  • 如果 onFulfilled 不是函式且 promise1 成功執行, promise2 必須成功執行並返回相同的值
  • 如果 onRejected 不是函式且 promise1 拒絕執行, promise2 必須拒絕執行並返回相同的據因

譯者注:理解上面的“返回”部分非常重要,即:不論 promise1 被 reject 還是被 resolve 時 promise2 都會被 resolve,只有出現異常時才會被 rejected

Promise 解決過程

Promise 解決過程是一個抽象的操作,其需輸入一個 promise 和一個值,我們表示為 [[Resolve]](promise, x),如果 xthen 方法且看上去像一個 Promise ,解決程式即嘗試使 promise 接受 x 的狀態;否則其用 x 的值來執行 promise

這種 thenable 的特性使得 Promise 的實現更具有通用性:只要其暴露出一個遵循 Promise/A+ 協議的 then 方法即可;這同時也使遵循 Promise/A+ 規範的實現可以與那些不太規範但可用的實現能良好共存。

執行 [[Resolve]](promise, x) 需遵循以下步驟:

xpromise 相等

如果 promisex 指向同一物件,以 TypeError 為據因拒絕執行 promise

x 為 Promise

如果 x 為 Promise ,則使 promise 接受 x 的狀態 注4

  • 如果 x 處於等待態, promise 需保持為等待態直至 x 被執行或拒絕
  • 如果 x 處於執行態,用相同的值執行 promise
  • 如果 x 處於拒絕態,用相同的據因拒絕 promise

x 為物件或函式

如果 x 為物件或者函式:

  • x.then 賦值給 then 注5
  • 如果取 x.then 的值時丟擲錯誤 e ,則以 e 為據因拒絕 promise
  • 如果 then 是函式,將 x 作為函式的作用域 this 呼叫之。傳遞兩個回撥函式作為引數,第一個引數叫做 resolvePromise ,第二個引數叫做 rejectPromise:
    • 如果 resolvePromise 以值 y 為引數被呼叫,則執行 [[Resolve]](promise, y)
    • 如果 rejectPromise 以據因 r 為引數被呼叫,則以據因 r 拒絕 promise
    • 如果 resolvePromiserejectPromise 均被呼叫,或者被同一引數呼叫了多次,則優先採用首次呼叫並忽略剩下的呼叫
    • 如果呼叫 then 方法丟擲了異常 e
      • 如果 resolvePromiserejectPromise 已經被呼叫,則忽略之
      • 否則以 e 為據因拒絕 promise
    • 如果 then 不是函式,以 x 為引數執行 promise
  • 如果 x 不為物件或者函式,以 x 為引數執行 promise

如果一個 promise 被一個迴圈的 thenable 鏈中的物件解決,而 [[Resolve]](promise, thenable) 的遞迴性質又使得其被再次呼叫,根據上述的演算法將會陷入無限遞迴之中。演算法雖不強制要求,但也鼓勵施者檢測這樣的遞迴是否存在,若檢測到存在則以一個可識別的 TypeError 為據因來拒絕 promise 注6

註釋


  • 注1 這裡的平臺程式碼指的是引擎、環境以及 promise 的實施程式碼。實踐中要確保 onFulfilledonRejected 方法非同步執行,且應該在 then 方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。這個事件佇列可以採用“巨集任務(macro-task)”機制或者“微任務(micro-task)”機制來實現。由於 promise 的實施程式碼本身就是平臺程式碼(譯者注:即都是 JavaScript),故程式碼自身在處理在處理程式時可能已經包含一個任務排程佇列。

    譯者注:這裡提及了 macrotask 和 microtask 兩個概念,這表示非同步任務的兩種分類。在掛起任務時,JS 引擎會將所有任務按照類別分到這兩個佇列中,首先在 macrotask 的佇列(這個佇列也被叫做 task queue)中取出第一個任務,執行完畢後取出 microtask 佇列中的所有任務順序執行;之後再取 macrotask 任務,周而復始,直至兩個佇列的任務都取完。

    兩個類別的具體分類如下:

    • macro-task: script(整體程式碼), setTimeout, setInterval, setImmediate, I/O, UI rendering
    • micro-task: process.nextTick, Promises(這裡指瀏覽器實現的原生 Promise), Object.observe, MutationObserver

    詳見 stackoverflow 解答這篇部落格

  • 注2 也就是說在嚴格模式(strict)中,函式 this 的值為 undefined ;在非嚴格模式中其為全域性物件。

  • 注3 程式碼實現在滿足所有要求的情況下可以允許 promise2 === promise1 。每個實現都要文件說明其是否允許以及在何種條件下允許 promise2 === promise1

  • 注4 總體來說,如果 x 符合當前實現,我們才認為它是真正的 promise 。這一規則允許那些特例實現接受符合已知要求的 Promises 狀態。

  • 注5 這步我們先是儲存了一個指向 x.then 的引用,然後測試並呼叫該引用,以避免多次訪問 x.then 屬性。這種預防措施確保了該屬性的一致性,因為其值可能在檢索呼叫時被改變。

  • 注6 實現不應該對 thenable 鏈的深度設限,並假定超出本限制的遞迴就是無限迴圈。只有真正的迴圈遞迴才應能導致 TypeError 異常;如果一條無限長的鏈上 thenable 均不相同,那麼遞迴下去永遠是正確的行為。

相關文章