[譯] 玩轉 JavaScript 面試:何爲 Promise ?

原文連接 Medium - Master the JavaScript Interview: What is a Promise?javascript

開門見山,何爲 Promise ?

一個promise指的是一個可能會在將來的某個時間點產生一個單一值的對象:不管是一個 resolved 值,仍是一個未 resolved 值的緣由(好比發生了網絡錯誤)。一個promise可能爲fulfilledrejectedpending三種狀態中的一種。promise用戶可使用回調函數來處理fulfilledrejected狀態。java

Promise能夠說是至關熱心了,其構造器一旦被調用,promise就會當即開始作你給它的任何任務。git

一個不太完整的 promise 發展史

早期對promisefutures(兩個概念相似/相關)的實現始於如 MultiLispConcurrent Prolog語言於 20 世紀 80年代早期的出現。promise一詞的使用是由 Barbara Liskov 和 Liuba Shrira 在 1988 年創造出來的1github

我第一次在 JavaScript 中知道promise這個概念時,Node纔剛剛出現,當時的社區也在積極的討論實現異步行爲的最佳方式。在一段時間裏,社區使用過promises這個概念,但最終落實在了標準Node錯誤處理回調上。promise

幾乎在同一時期,Dojo框架中經過Deferred API添加了promises。隨着公衆對此持續不斷興趣和活躍度的高漲,最終造成了一個新的promise/A規範使得多種promises的實現得以統一。網絡

jQuery 的異步行爲圍繞 promises 被重構。jQuery 對 promise 的支持與 Dojo 的 Deferred極其相似,也很快因其大規模受衆而成爲 JavaScript 中最受歡迎的 promise 實現方式。然而,jQuery 不支持fulfilled/rejected兩個通道的鏈式調用行爲和異常處理,而這些特性也是用戶賴以使用 promise 構建應用的基礎。app

儘管 jQuery 存在上述缺點,其依然成爲當時 JavaScript promises 的主流實現,受喜好程度遙遙領先於像是QWhen或者Bluebird這樣的 promise 庫。jQuery 實現的不兼容性催生了一些對 promise 的補充說明,從而造成了Promises/A+規範。框架

ES6 中的 Promise 帶來了對 Promises/A+ 規範的徹底兼容,另外還有一些很是重要的 API 也基於新的標準 Promise 而獲得支持:最多見的有WHATWG Fetch規範和異步函數標準。異步

這裏說明了 promises 是符合 Promises/A+ 規範的,也是 ECMAScript 標準Promise的實現。函數

Promise 是如何工做的

promise 是一個能夠從一個異步函數中返回的異步對象,它能夠處於如下三種狀態中的一種:

  • Fulfilled:onFulfilled()會被調用(好比resolve()被調用)
  • Rejected:onRejected()會被調用(好比reject()被調用)
  • Pending: 暫時還未fulfilledrejected

promise 的狀態只要不是 pending 即表明其已肯定狀態(resolved 或 rejected),有時人們會用 resolved 和 settled 來表示同一個意思:非 pending狀態。

狀態一旦肯定,promise 的狀態就不能再被改變,調用 resolve()reject()也不會產生任何影響。一個已肯定狀態的 promise 的不可變性是其一大重要特性。

原生 JS promise 不對外暴露狀態。實際上你可能更但願把它當作一個黑盒機制來看待。只有當某函數的做用是建立一個 promise 時、或者是去訪問 resolve 或 reject 時咱們才須要深刻 promise 的狀態。

下面的函數會在指定時間後 resolve,而後返回一個 promise:

const wait = time => new Promise((resolve) => setTimeout(resolve, reject))

wait(3000).then(() => console.log('Hello!'));
複製代碼

這裏調用wait(3000)會在等待 3000ms 後打印出 Hello!。全部符合標準的 promises 都會定義一個.then()方法,可向該方法中傳遞一個句柄,從而拿到 resolve 或 reject 的值。

ES6 的 promise 構造函數接收一個函數做爲參數。該函數接收兩個參數分別爲resolve()reject()。在上面的例子中,咱們只用到了resolve(),而後調用了setTimeout()建立一個延遲函數,最後在延遲函數執行完後調用resolve()

你能夠選擇只傳給resolve()reject()一個值,值會被傳遞到.then()中的回調函數中。

每當我向reject()中傳一個值時,我都會傳一個 Error 對象進去。通常來講我指望兩種解決狀態:正常的圓滿結局,或者是拋出一個異常。傳一個 Error 對象進去會使得結果更明朗。

幾個重要的 Promise 規則

promises 的標準已經由Promises/A+ 規範社區定義好了。現存的不少種實現都遵照該規範,這其中就包括 JavaScript 標準 ECMAScript promises。

遵循上述規範的 promises 必須包含如下幾點規則:

  • 一個 promise/thenable 對象必須提供一個標準的兼容的 .then()方法;
  • 處於 pending 狀態的 promise 能夠改成 fulfilled 或 rejected 兩種狀態;
  • 一個處於 fulfilled 或 rejected 中的 promise 狀態一旦肯定,就不再能改變爲其餘狀態;
  • 一旦 promise 狀態肯定下來,它必須有一個值(即便是 undefined)。該值不可被改變。

每一個 promise 必須提供一個具有以下特性的.then()方法:

promise.then(
  onFulfilled?: Function,
  onRejected?: Function
) => Promise
複製代碼

.then()方法必須符合下面的規則:

  • onFulfilled()onRejected()皆爲可選參數;
  • 若是所提供的參數不是函數,參數將被忽略;
  • onFulfilled()會在 promise 的狀態變爲 fulfilled 時調用,promise 返回的值會被做爲第一個參數;
  • onRejected()會在 promise 的狀態變爲 rejected 時調用,被拒絕的緣由會被做爲第一個參數。緣由可能會是任何有效的 JavaScript 值,可是因爲被拒絕基本上等同於拋出異常,因此我建議使用 Error 對象;
  • onFulfilled()onRejected()都不會被屢次調用;
  • .then()可能會在同一個 promise 上被調用屢次。換句話說,promise 可被用來合併回調函數;
  • .then()必須返回一個新的 promise,可稱之爲promise2
  • 若是onFulfilled()onRejected()返回一個值爲xx是一個 promise,promise2將用x鎖定。不然,promise2會被值x fulfilled。
  • 若是onFulfilled()onRejected()拋出一個異常epromise2必須以e做爲緣由被 rejected;
  • 若是onFulfilled()不是函數,promise1被 fulfilled,那麼promise2必須以相同的值被 fulfilled;
  • 若是onRejected()不是函數,promise1被 rejected,那麼promise2必須以相同的緣由被 rejected;

Promise 鏈式調用

因爲.then()老是返回一個新的 promise,這樣就能夠實現對鏈式 promise 中的錯誤進行精確控制。Promises 容許咱們模仿正常的同步代碼行爲(如 try...catch)。

就像同步代碼同樣,鏈式調用能夠產生順序執行的效果。好比下面這樣:

fetch(url)
  .then(process)
  .then(save)
  .catch(handleErrors)
;
複製代碼

假設上面的fetch()process()save()都返回 promises,process()會等待fetch()執行完畢後再開始執行,同理save()也要等待process()執行完畢纔開始執行,handleErrors()當且僅當前面的任何一個 promises 運行出錯纔會執行。

下面給出一個複雜的例子:

const wait = time => new Promise(
  res => setTimeout(() => res(), time)
);

wait(200)
  // onFulfilled() 能夠返回一個新的 promise, `x`
  .then(() => new Promise(res => res('foo')))
  // 下一個 promise 會假設 `x`的狀態
  .then(a => a)
  // 上面咱們返回了未被包裹的`x`的值
  // 所以上面的`.then()`返回了一個 fulfilled promise
  // 有了上面的值以後:
  .then(b => console.log(b)) // 'foo'
  // 須要注意的是 `null` 是一個有效的 promise 返回值:
  .then(() => null)
  .then(c => console.log(c)) // null
  // 至此還未報錯:
  .then(() => {throw new Error('foo');})
  // 相反, 返回的 promise 是 rejected
  // error 的緣由以下:
  .then(
    // 因爲上面的 error致使在這裏啥都沒打印:
    d => console.log(`d: ${ d }`),
    // 如今咱們處理這個 error (rejection 的緣由)
    e => console.log(e)) // [Error: foo]
  // 有了以前的異常處理, 咱們能夠繼續:
  .then(f => console.log(`f: ${ f }`)) // f: undefined
  // 下面的代碼未打印任何東西. e 已經被處理過了,
  // 因此該句柄並未被調用:
  .catch(e => console.log(e))
  .then(() => { throw new Error('bar'); })
  // 當一個 promise 被 rejected, success 句柄就被跳過.
  // 這裏由於 'bar' 異常而不打印任何東西:
  .then(g => console.log(`g: ${ g }`))
  .catch(h => console.log(h)) // [Error: bar]
;
複製代碼

錯誤處理

須要注意的是 promise 同時具備成功和失敗的句柄,因此下面代碼的寫法很常見:

save().then(
    handleSuccess,
    handleError
)
複製代碼

可是若是 handleSuccess()出錯了怎麼辦?從.then()中返回的 promise 就會被 rejected,可是後續就沒有能捕獲該錯誤信息的函數了 —— 意思就是你 app 中的一個錯誤被吞掉了,這可有點兒糟糕。

針對上述緣由,有人就將上面的代碼稱爲一種反模式(anti-pattern),並建議使用以下寫法替代:

save()
    .then(handleSuccess)
    .catch(handleError);
複製代碼

其中的差別很微妙,但卻很重要。在頭一個例子中,來自save()中的錯誤會被捕獲,可是來自handleSuccess()中的錯誤就會被吞掉。

在第二個例子中, .catch()會處理來自不管是 save()仍是 handleSuccess()中的錯誤。
固然了,來自於 save()的錯誤還有多是網絡錯誤,而 handleSuccess()中的錯誤可能來自於開發者忘記處理一個錯誤的狀態碼,要是你想對這兩種錯誤進行不一樣的處理該怎麼辦?那就能夠選擇下面這種處理方式了:

save()
    .then(
        handleSuccess,
        handleNetworkError
    )
    .catch(handleProgrammerError)
複製代碼

不管你傾向於哪一種方式,我都推薦你在全部的 promises 後面帶上 .catch()

要如何取消/中斷一個 Promise

剛學會使用 promise 的用戶老是有不少疑問,其中最多的就是關於如何取消/中斷一個 promise。思路是這樣的:直接去 reject 想要取消/中斷的 promise,緣由就寫「Cancelled」便可。但若是你要將它與常規錯誤處理方式區分開來的話,那就去開發本身的錯誤處理分支。

下面列出了幾種人們在寫取消/中斷 promise 時常犯的錯誤:

給 promise 添加了一個.cancel()

添加.cancel()使得 promise 非標準化了,同時也違背了 promise 的另外一個規定:只有建立了 promise 的函數纔有能力去 resolve、reject 或 取消/中斷該 promise。傳播這種寫法只會破壞函數的封裝特性,慫恿人們在不恰當的地方操做 promise 代碼,破壞了 promise。

忘記清理

有些聰明的人搞清楚了使用promise.race()的方式來取消/中斷 promise。這種方式的問題在於中斷控制的操做是由建立該 promise 的函數發起的,這也是惟一一處恰當的進行清理動做的位置,好比說清理定時器或者經過解除對數據的引用來釋放內存等等。

忘記處理一個被 reject 的中斷 promise

你知道當你忘記處理一個 promise 的拒絕狀態時 Chrome 拋出的滿控制檯的警告信息嗎?

從新思考 promise 取消/中斷

通常來講,我會在一個 promise 建立時就把 promise 全部須要的信息都傳給它,以便 promise 決定如何進行 resolve/reject/cancel。這種方式並不須要一個 .cancel()方法附着在 promise 上。你可能想知道的是怎麼才能知道是否要在 promise 建立時知道它將要被取消。

咱們要傳的那個決定是否要取消的值能夠是 promise 本身,看起來可能像下面這樣:

const wait = (
  time,
  cancel = Promise.reject()
) => new Promise((resolve, reject) => {
  const timer = setTimeout(resolve, time);
  const noop = () => {};

  cancel.then(() => {
    clearTimeout(timer);
    reject(new Error('Cancelled'));
  }, noop);
});

const shouldCancel = Promise.resolve(); // Yes, cancel
// const shouldCancel = Promise.reject(); // No cancel

wait(2000, shouldCancel).then(
  () => console.log('Hello!'),
  (e) => console.log(e) // [Error: Cancelled]
); 
複製代碼

這裏使用了默認分配的參數告訴它默認是不取消的。這樣使得cancel參數是可選的。而後咱們設置一個定時器,這裏咱們拿到計時器的 ID 以便於後面取消它。

咱們使用cancel.then()來處理取消/中斷和資源的清理。它的運行條件是在 resolve 以前讓 promise 取消。若是你取消的過晚,你就錯過了取消的時機。

你可能比較好奇noop函數的做用是啥,noop 一詞表示空操做,意指啥都不作。要是不指定這個函數,V8 引擎會拋出警告:UnhandledPromiseRejectionWarning: Unhandled promise rejection,因此老是記得去處理 promise 的 rejection 是個好習慣,即便你的句柄爲 noop。

抽象的 promise 取消/中斷

上面的wait()計時器固然是極好的,但咱們要繼續將上面這種思路作進一步的抽象,來封裝全部你須要知道的東西:

  1. 默認 reject 須要中斷的 promise
  2. 記得要清理被 reject 過的 promise
  3. 保持警戒,onCancel的清理操做自己也有可能拋異常,該異常也須要處理。

讓咱們來建立一個可中斷的 promise 工具函數吧,這樣你就能夠用來包裹任何 promise 了。形式以下:

speculation(fn: SpecFunction, shouldCancel: Promise) => Promise
複製代碼

SpecFunction就像你傳入 Promise 構造器中的函數同樣,惟一的不一樣在於它有一個onCancel句柄:

SpecFunction(resolve: Function, reject: Function, onCancel: Function) => Void
複製代碼
const speculation = (
  fn,
  cancel = Promise.reject() 
) => new Promise((resolve, reject) => {
  const noop = () => {};

  const onCancel = (
    handleCancel
  ) => cancel.then(
      handleCancel,
      noop
    )
    .catch(e => reject(e))
  ;

  fn(resolve, reject, onCancel);
});
複製代碼

上例只是其做用要旨,其實還有須要邊界狀況須要你去考慮。我本身寫了一個完整的版本供你們參考,speculation

結語

文章太長,翻譯到後半段着實翻譯不下去了,主要仍是自身對 promise 的理解還不夠深,後面就看不懂了,但仍是以爲要善始善終,把這件事作完,後面懂了再回頭完善,never giveup!

參考文獻

[1] Barbara Liskov; Liuba Shrira (1988). 「Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems」. Proceedings of the SIGPLAN ’88 Conference on Programming Language Design and Implementation; Atlanta, Georgia, United States, pp. 260–267. ISBN 0–89791–269–1, published by ACM. Also published in ACM SIGPLAN Notices, Volume 23, Issue 7, July 1988.

相關文章
相關標籤/搜索