ES6標準出爐之前,一個幽靈,回撥的幽靈,遊蕩在JavaScript世界。
正所謂:
世界本沒有回撥,寫的人多了,也就有了
})})})})})
。
Promise
的興起,是因為非同步方法呼叫中,往往會出現回撥函式一環扣一環的情況。這種情況導致了回撥金字塔問題的出現。不僅程式碼寫起來費勁又不美觀,而且問題複雜的時候,閱讀程式碼的人也難以理解。
舉例如下:
1 2 3 4 5 6 7 8 9 10 |
db.save(data, function(data){ // do something... db.save(data1, function(data){ // do something... db.save(data2, function(data){ // do something... done(data3); // 返回資料 }) }); }); |
假設有一個資料庫儲存操作,一次請求需要在三個表中儲存三次資料。那麼我們的程式碼就跟上面的程式碼相似了。這時候假設在第二個db.save
出了問題怎麼辦?基於這個考慮,我們又需要在每一層回撥中使用類似try...catch
這樣的邏輯。這個就是萬惡的來源,也是node剛開始廣為詬病的一點。
另外一個缺點就是,假設我們的三次儲存之間並沒有前後依賴關係,我們仍然需要等待前面的函式執行完畢, 才能執行下一步,而無法三個儲存並行,之後返回一個三個儲存過後需要的結果。(或者說實現起來需要技巧)
不幸的是,在我剛開始接觸node的時候,我寫了大量這樣的hell。
作為一個有時還動下腦子的程式設計師,我嘗試了樸靈大人的eventproxy。後來因為還是寫前端程式碼多一些,我接觸了ES6,發現了一個解決回撥深淵的利器Promise
。
其實早在ES6的Promise
之前,Q
,when.js
,bluebird
等等庫早就根據Promise
標準(參考Promise/A+)造出了自己的promise
輪子。
(看過一篇文章,我覺得很有道理。裡面說,不要擴充套件內建的原生物件。這種做法是不能面向未來的。所以這裡有一個提示:使用擴充套件原生Promise
的庫時,需要謹慎。)
這裡僅討論原生的Promise
。
ES6 Promise
Promise物件狀態
在詳解Promise
之前,先來點理論:
Promise/A+
規範, 規定Promise物件是一個有限狀態機。它三個狀態:
pending
(執行中)fulfilled
(成功)reject
(拒絕)
其中pending為初始狀態,fulfilled和rejected為結束狀態(結束狀態表示promise的生命週期已結束)。
狀態轉換關係為:
1 |
pending->fulfilled,pending->rejected。 |
隨著狀態的轉換將觸發各種事件(如執行成功事件、執行失敗事件等)。
Promise形式
Promise的長相就像這樣子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var promise = new Promise(function func(resolve, reject){ // do somthing, maybe async if (success){ return resolve(data); } else { return reject(data); } }); promise.then(function(data){ // do something... e.g console.log(data); }, function(err){ // deal the err. }) |
這裡的變數promise
是Promise
這個物件的例項。
promise物件在建立的時候會執行func
函式中的邏輯。
邏輯處理完畢並且沒有錯誤時,resolve
這個回撥會將值傳遞到一個特殊的地方。這個特殊的地方在哪呢?就是下面程式碼中的then
,我們使用then
中的回撥函式來處理resolve後的結果。比如上面的程式碼中,我們將值簡單的輸出到控制檯。如果有錯誤,則reject
到then
的第二個回撥函式中,對錯誤進行處理。
配合上面的有限狀態機的理論,我們知道在Promise
建構函式中執行回撥函式程式碼時,狀態為pending
,resolve
之後狀態為fulfilled
,reject
之後狀態為reject
Promise資料流動
以上是promise的第一次資料流動情況。
比較funny的是,promise的then
方法依然能夠返回一個Promise
物件,這樣我們就又能用下一個then
來做一樣的處理。
第一個then
中的兩個回撥函式決定第一個then
返回的是一個什麼樣的Promise
物件。
- 假設第一個
then
的第一個回撥沒有返回一個Promise
物件,那麼第二個then
的呼叫者還是原來的Promise物件,只不過其resolve
的值變成了第一個then
中第一個回撥函式的返回值。 - 假設第一個
then
的第一個回撥函式返回了一個Promise
物件,那麼第二個then
的呼叫者變成了這個新的Promise
物件,第二個then
等待這個新的Promise
物件resolve
或者reject
之後執行回撥。
話雖然饒了一點,但是我自我感覺說的還是很清楚的呢。哈哈~
如果任意地方遇到了錯誤,則錯誤之後交給遇到的第一個帶第二個回撥函式的then
的第二個回撥函式來處理。可以理解為錯誤一直向後reject
, 直到被處理為止。
另外,Promise
物件還有一個方法catch
,這個方法接受一個回撥函式來處理錯誤。即:
1 2 3 |
promise.catch(function(err){ // deal the err. }) |
假設對錯誤的處理是相似的,這個方法可以對錯誤進行集中統一處理。所以其他的then
方法就不需要第二個回撥啦~
控制併發的Promise
Promise有一個”靜態方法”——Promise.all
(注意並非是promise.prototype
), 這個方法接受一個元素是Promise物件的陣列。
這個方法也返回一個Promise
物件,如果陣列中所有的Promise
物件都resolve了,那麼這些resolve的值將作為一個陣列作為Promise.all
這個方法的返回值的(Promise
物件)的resolve值,之後可以被then
方法處理。如果陣列中任意的Promise
被reject
,那麼該reject
的值就是Promise.all
方法的返回值的reject
值.
很op的一點是:
then方法的第一個回撥函式接收的resolve值(如上所述,是一個陣列)的順序和Promise.all中引數陣列的順序一致,而不是按時間順序排序。
還有一個和Promise.all
相類似的方法Promise.race
,它同樣接收一個陣列,只不過它只接受第一個被resolve的值。
將其他物件變為Promise物件
Promise.resovle
方法,可以將不是Promise物件作為引數,返回一個Promise
物件。
有兩種情形:
- 假設傳入的引數沒有一個
.then
方法,那麼這個返回的Promise
物件變成了resolve狀態,其resolve的值就是這個物件本身。 - 假設傳入的引數帶有一個
then
方法(稱為thenable
物件), 那麼將這個物件的型別變為Promise
,其then
方法變成Promise.prototype.then
方法。
Promise是解決非同步的方案嗎?
最後說一點很重要的事:Promise
的作用是解決回撥金字塔的問題,對於控制非同步流程實際上沒有起到很大的作用。真正使用Promise
對非同步流程進行控制,我們還要藉助ES6 generator
函式。(例如Tj大神的co
庫的實現)。
然而ES7將有一個更加牛逼的解決方案:async/await
,這個方案類似於co
,但是加了原生支援。拭目以待吧。
文件
以上。一點微小的見解,謝謝大家。