原文連接 Medium - Master the JavaScript Interview: What is a Promise?javascript
一個promise
指的是一個可能會在將來的某個時間點產生一個單一值的對象:不管是一個 resolved 值,仍是一個未 resolved 值的緣由(好比發生了網絡錯誤)。一個promise
可能爲fulfilled
、 rejected
或 pending
三種狀態中的一種。promise
用戶可使用回調函數來處理fulfilled
和 rejected
狀態。java
Promise
能夠說是至關熱心了,其構造器一旦被調用,promise
就會當即開始作你給它的任何任務。git
早期對promise
和futures
(兩個概念相似/相關)的實現始於如 MultiLisp
和Concurrent Prolog
語言於 20 世紀 80年代早期的出現。promise
一詞的使用是由 Barbara Liskov 和 Liuba Shrira 在 1988 年創造出來的1。github
我第一次在 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 的主流實現,受喜好程度遙遙領先於像是Q
、When
或者Bluebird
這樣的 promise 庫。jQuery 實現的不兼容性催生了一些對 promise 的補充說明,從而造成了Promises/A+
規範。框架
ES6 中的 Promise 帶來了對 Promises/A+ 規範的徹底兼容,另外還有一些很是重要的 API 也基於新的標準 Promise 而獲得支持:最多見的有WHATWG Fetch
規範和異步函數標準。異步
這裏說明了 promises 是符合 Promises/A+ 規範的,也是 ECMAScript 標準Promise
的實現。函數
promise 是一個能夠從一個異步函數中返回的異步對象,它能夠處於如下三種狀態中的一種:
onFulfilled()
會被調用(好比resolve()
被調用)onRejected()
會被調用(好比reject()
被調用)fulfilled
或rejected
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 對象進去會使得結果更明朗。
promises 的標準已經由Promises/A+ 規範社區定義好了。現存的不少種實現都遵照該規範,這其中就包括 JavaScript 標準 ECMAScript promises。
遵循上述規範的 promises 必須包含如下幾點規則:
.then()
方法;每一個 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()
返回一個值爲x
,x
是一個 promise,promise2
將用x
鎖定。不然,promise2
會被值x
fulfilled。onFulfilled()
或onRejected()
拋出一個異常e
,promise2
必須以e
做爲緣由被 rejected;onFulfilled()
不是函數,promise1
被 fulfilled,那麼promise2
必須以相同的值被 fulfilled;onRejected()
不是函數,promise1
被 rejected,那麼promise2
必須以相同的緣由被 rejected;因爲.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。思路是這樣的:直接去 reject 想要取消/中斷的 promise,緣由就寫「Cancelled」便可。但若是你要將它與常規錯誤處理方式區分開來的話,那就去開發本身的錯誤處理分支。
下面列出了幾種人們在寫取消/中斷 promise 時常犯的錯誤:
.cancel()
添加.cancel()
使得 promise 非標準化了,同時也違背了 promise 的另外一個規定:只有建立了 promise 的函數纔有能力去 resolve、reject 或 取消/中斷該 promise。傳播這種寫法只會破壞函數的封裝特性,慫恿人們在不恰當的地方操做 promise 代碼,破壞了 promise。
有些聰明的人搞清楚了使用promise.race()
的方式來取消/中斷 promise。這種方式的問題在於中斷控制的操做是由建立該 promise 的函數發起的,這也是惟一一處恰當的進行清理動做的位置,好比說清理定時器或者經過解除對數據的引用來釋放內存等等。
你知道當你忘記處理一個 promise 的拒絕狀態時 Chrome 拋出的滿控制檯的警告信息嗎?
通常來講,我會在一個 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。
上面的wait()
計時器固然是極好的,但咱們要繼續將上面這種思路作進一步的抽象,來封裝全部你須要知道的東西:
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.