文章出處

上一篇我們講解了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 必須拒絕執行并返回相同的據因

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對象即可。例如:
runAsync1()
.then(function(data){
    console.log(data);
    return runAsync2(); //返回值為promise對象
})
.then(function(data){
    console.log(data);
    return runAsync3();
})

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+,列出來供大家參考:
 
對于規范,有些同學不太想看,我平時在面試的時候問起一些規范相關的問題,大多數面試者都回答不來。有些人或許會說,作為司機會開車不就行了,難道要知道汽車是怎么造的嗎?那我這里想反問一下,你準備當一輩子司機嗎?對于規范可以不那么充分研究,但是起碼得知道關鍵部分,有這樣一個意識,對于以后自己成長為大牛也有所幫助。

文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()