大白話講解Promise(二)理解Promise規範

發表於2016-03-27
上一篇我們講解了ES6中Promise的用法,但是知道了用法還遠遠不夠,作為一名專業的前端工程師,還必須通曉原理。所以,為了補全我們關於Promise的知識樹,有必要理解Promise/A+規範,理解了它你才能知道Promise內部是怎麼回事,我們ES6中的Promise是如何一路走來的。
網上關於Promise/A+的翻譯文件很多,所以我就不翻譯一次了,本篇的目的在於為文件增加一些標註,以幫助我們更好的理解。翻譯內容引用自:http://malcolmyu.github.io/malnote/2015/06/12/Promises-A-Plus/,部分我認為不太合適的有作修改。
 

術語


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)
引數可選

onFulfilled 和 onRejected 都是可選引數。

  • 如果 onFulfilled 不是函式,其必須被忽略
  • 如果 onRejected 不是函式,其必須被忽略
注:如果我們只想傳onRejected而不想傳onFulfilled,可以這麼寫:pormise.then(null, onRejected)

onFulfilled 特性

如果 onFulfilled 是函式:

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

onRejected 特性

如果 onRejected 是函式:

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

呼叫時機

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

呼叫要求

onFulfilled 和 onRejected 必須被作為函式呼叫(即沒有 this 值)注2
 
注:也就是說,我們在promise中就別用this了。

多次呼叫

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

  • 當 promise 成功執行時,所有 onFulfilled 需按照其註冊順序依次回撥
  • 當 promise 被拒絕執行時,所有的 onRejected 需按照其註冊順序依次回撥
注:這裡解釋了我們可以鏈式呼叫,promise.then().then().then()

返回

then 方法必須返回一個 promise 物件 注3
promise2 = promise1.then(onFulfilled, onRejected);
注:這就是我們能夠進行鏈式呼叫的原因,因為then方法返回的還是一個promise物件。
如果 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),如果 x 有then 方法且看上去像一個 Promise ,解決程式即嘗試使 promise 接受 x 的狀態;否則其用 x 的值來執行 promise 。

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

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

x 與 promise 相等

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

x 為 Promise

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

  • 如果 x 處於等待態, promise 需保持為等待態直至 x 被執行或拒絕
  • 如果 x 處於完成態,用相同的值執行 promise
  • 如果 x 處於拒絕態,用相同的據因拒絕 promise
注:這裡就是解釋我們鏈式呼叫then時,可以繼續進行非同步操作,只要在onFulfilled中繼續返回一個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
    • 如果 resolvePromise 和 rejectPromise 均被呼叫,或者被同一引數呼叫了多次,則優先採用首次呼叫並忽略剩下的呼叫
    • 如果呼叫 then 方法丟擲了異常 e
      • 如果 resolvePromise 或 rejectPromise 已經被呼叫,則忽略之
      • 否則以 e 為據因拒絕 promise
    • 如果 then 不是函式,以 x 為引數執行 promise
  • 如果 x 不為物件或者函式,以 x 為引數執行 promise

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

註釋


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

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

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

    • macro-task: script(整體程式碼), setTimeoutsetIntervalsetImmediate, I/O, UI rendering
    • micro-task: process.nextTickPromises(這裡指瀏覽器實現的原生 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 均不相同,那麼遞迴下去永遠是正確的行為。
補充:Promise/A+並未規定all和race方法,也就是說這兩個方法是ES6自己增加的了。因為Promise/A+只是規範,ES6是做了自己的實現,當然可以自己加了。實現Promise規範的庫有很多,比如jquery、dojo等,jquery在實現的時候還增加了更多的方法,我們在下一篇會做講解。網上也有不少朋友自己實現過Promise/A+,列出來供大家參考:
對於規範,有些同學不太想看,我平時在面試的時候問起一些規範相關的問題,大多數面試者都回答不來。有些人或許會說,作為司機會開車不就行了,難道要知道汽車是怎麼造的嗎?那我這裡想反問一下,你準備當一輩子司機嗎?對於規範可以不那麼充分研究,但是起碼得知道關鍵部分,有這樣一個意識,對於以後自己成長為大牛也有所幫助。

相關文章