深刻理解 Promise

自從ES6流行起來,Promise 的使用變得更頻繁更普遍了,好比異步請求通常返回一個 Promise 對象,Generator 中 yield 後面通常跟 Promise 對象,ES7中 Async 函數中 await 後面通常也是 Promise 對象,還有更多的 NodeAPI 也會返回 Promise 對象,能夠說如今的編程中 Promise 的使用無處不在,那麼咱們是否真的弄懂了 Promise 呢?是否有誤用或錯誤使用 Promise 呢?是否知道 Promise 的實現原理和 Promise 的花樣玩法呢?下面讓咱們一塊兒來探討一下吧。git

 

Promise 規範

這裏只列舉規範中的大體內容,詳細內容請查看 Promises/A+ 中文 ,這是ES6 Promises的前身,是一個社區規範,它和 ES6 Promises 有不少共通的內容。es6

  1. 狀態 Promise 的初始狀態是 Pending ,狀態只能被轉換爲(Resolved)FulfilledRejected,狀態的轉換不可逆。
  2. then 必須有 then 方法,接收兩個可選函數參數onFulfilledonRejectedthen方法必須返回一個新的 Promise 對象,爲了保證 then 中回調的執行順序,回調必須使用異步執行。
  3. 兼容 不一樣的 Promise 的實現必須能夠互相調用

具體標準的實現將在 中篇 - 手動封裝 中詳細說明github

 

ES6 Promise API

若是你對 Promise的使用 還不是很瞭解,可參考閱讀如下資料:ajax

這裏只對ES6 Promise API作簡要說明編程

 

實例方法

  • .then(resolvedFn, rejectFn) : 爲Promise實例添加狀態改變時的回調,返回值是一個 新的Promise實例
  • .catch() : 是 .then(null, rejectFn) 的語法糖,返回值也是一個 新的Promise對象
    Promise對象的錯誤具備冒泡性質,錯誤會不斷的向後傳遞,直到 .catch() 捕獲
    正由於 then 和 catch 返回的都是 Promise 對象,因此才能夠不斷的鏈式調用

 

靜態方法

  • Promise.resolve()  
    • 將現有對象轉換爲Promise對象
    • 若是參數是promise實例,則直接返回這個實例
    • 若是參數是thenabled對象(有then方法的對象),則先將其轉換爲promise對象,而後當即執行這個對象的then方法
    • 若是參數是個原始值,則返回一個promise對象,狀態爲resolved,這個原始值會傳遞給回調
    • 沒有參數,直接返回一個resolved的Promise對象
  • Promise.reject()
    • 同上,不一樣的是返回的promise對象的狀態爲rejected
  • Promise.all()
    • 接收一個Promise實例的數組或具備Iterator接口的對象,
    • 若是元素不是Promise對象,則使用Promise.resolve轉成Promise對象
    • 若是所有成功,狀態變爲resolved,返回值將組成一個數組傳給回調
    • 只要有一個失敗,狀態就變爲rejected,返回值將直接傳遞給回調
    • all() 的返回值也是新的Promise對象
  • Promise.race()
    • 同上,區別是,只要有一個Promise實例率先發生變化(不管是狀態變成resolved仍是rejected)都觸發then中的回調,返回值將傳遞給回調
    • race()的返回值也是新的Promise對象
  •  

Polyfill和擴展類庫

Polyfill

只須要在瀏覽器中加載Polyfill類庫,就能使用IE10等或者尚未提供對Promise支持的瀏覽器中使用Promise裏規定的方法。json

calvinmetcalf/lie 很是簡潔的 promise 庫,中篇中的手動封裝實現就是參考了這個庫
jakearchibald/es6-promise 兼容 Promises/A+ 的類庫, 它只是 RSVP.js 的一個子集,只實現了Promises 規定的 API。
yahoo/ypromise 這是一個獨立版本的 YUI 的 Promise Polyfill,具備和 ES6 Promises 的兼容性segmentfault

 

Promise擴展類庫

Promise擴展類庫除了實現了Promise中定義的規範以外,還增長了本身獨自定義的功能。api

kriskowal/q 類庫 Q 實現了 Promises 和 Deferreds 等規範。 它自2009年開始開發,還提供了面向Node.js的文件IO API Q-IO 等, 是一個在不少場景下都能用獲得的類庫。
petkaantonov/bluebird這個類庫除了兼容 Promise 規範以外,還擴展了取消promise對象的運行,取得promise的運行進度,以及錯誤處理的擴展檢測等很是豐富的功能,此外它在實現上還在性能問題下了很大的功夫。數組

注意
在項目中,有可能兩個不一樣的模塊使用的是兩個不一樣的Promise類庫,那麼在大部分的Promise的實現中,都是遵循 Promise/A+ 標準和兼容ES6 Promise接口的,也是不一樣的Promise的實現是能夠互相調用的,如何調用,將在下面說明。promise

 

錯誤用法及誤區

看成回調來用 Callback Hell

loadAsync1().then(function(data1) {
  loadAsync2(data1).then(function(data2) {
    loadAsync3(data2).then(okFn, failFn)
  });
});

Promise是用來解決異步嵌套回調的,這種寫法雖然可行,但違背了Promise的設計初衷
改爲下面的寫法,會讓結構更加清晰

loadAsync1()
    .then(function(data1) {
        return loadAsync2(data1)
    })
    .then(function(data2){
        return loadAsync3(data2)
    })
    .then(okFn, failFn)

沒有返回值

loadAsync1()
    .then(function(data1) {
        loadAsync2(data1)
    })
    .then(function(data2){
        loadAsync3(data2)
    })
    .then(res=>console.log(res))

promise 的神奇之處在於讓咱們可以在回調函數裏面使用 return 和 throw, 因此在then中能夠return出一個promise對象或普通的值,也能夠throw出一個錯誤對象,但若是沒有任何返回,將默認返回 undefined,那麼後面的then中的回調參數接收到的將是undefined,而不是上一個then中內部函數 loadAsync2 執行的結果,後面都將是undefined。

沒有Catch

loadAsync1()
    .then(function(data1) {
        return loadAsync2(data1)
    })
    .then(function(data2){
        return loadAsync3(data2)
    })
    .then(okFn, failFn)

這裏的調用,並無添加catch方法,那麼若是中間某個環節發生錯誤,將不會被捕獲,控制檯將看不到任何錯誤,不利於調試查錯,因此最好在最後添加catch方法用於捕獲錯誤。

添加catch

loadAsync1()
    .then(function(data1) {
        return loadAsync2(data1)
    })
    .then(function(data2){
        return loadAsync3(data2)
    })
    .then(okFn, failFn)
    .catch(err=>console.log(err))

catch()與then(null, fn)

在有些狀況下catch與then(null, fn)並不等同,以下

ajaxLoad1()
    .then(res=>{ return ajaxLoad2() })
    .catch(err=> console.log(err))

此時,catch捕獲的並非ajaxLoad1的錯誤,而是ajaxLoad2的錯誤,因此有時候,二者仍是要結合起來使用:

ajaxLoad1()
    .then(res=>{ return ajaxLoad2() }, err=>console.log(err))
    .catch(err=> console.log(err))

斷鏈 The Broken Chain

function loadAsyncFnX(){ return Promise.resolve(1); }
function doSth(){ return 2; }
function asyncFn(){
    var promise = loadAsyncFnX()
    promise.then(function(){
        return doSth();
    })
    return promise;
}
asyncFn().then(res=>console.log(res)).catch(err=>console.log(err))
// 1

上面這種用法,從執行結果來看,then中回調的參數其實並非doSth()返回的結果,而是loadAsyncFnX()返回的結果,catch 到的錯誤也是 loadAsyncFnX()中的錯誤,因此 doSth() 的結果和錯誤將不會被後而的then中的回調捕獲到,造成了斷鏈,由於 then 方法將返回一個新的Promise對象,而不是原來的Promise對象。

改寫以下

function loadAsyncFnX(){ return Promise.resolve(1); }
function doSth(){ return 2; }
function asyncFn(){
    var promise = loadAsyncFnX()
    return promise.then(function(){
        return doSth();
    })
}
asyncFn().then(res=>console.log(res)).catch(err=>console.log(err))
// 2

穿透 Fall Through

new Promise(resolve=>resolve(8))
  .then(1)
  .catch(null)
  .then(Promise.resolve(9))
  .then(res=> console.log(res))
// 8

這裏,若是then或catch接收的不是函數,那麼就會發生穿透行爲,因此在應用過程當中,應該保證then接收到的參數始終是一個函數。

長度未知的串行與並行

並行執行

getAsyncArr()
    .then(promiseArr=>{
        var resArr = [];
        promiseArr.forEach(v=>{
            v().then(res=> resArr.push(res))
        })
        return resArr;
    })
    .then(res=>console.log(res))

使用forEach遍歷執行promise,在上面的實現中,第二個then有可能拿到的是空的結果或者不完整的結果,由於,第二個then的回調沒法預知 promiseArr 中每個promise是否都執行完成,那麼這裏可使用 Promise.all 結合 map 方法去改善

getAsyncArr()
    .then(promiseArr=>{
        return Promise.all(promiseArr);
    })
    .then(res=>console.log(res))

若是須要串行執行,那和咱們能夠利用數據的reduce來處理串行執行

var pA = [
    function(){return new Promise(resolve=>resolve(1))},
    function(data){return new Promise(resolve=>resolve(1+data))},
    function(data){return new Promise(resolve=>resolve(1+data))}
]
pA.reduce((prev, next)=>prev.then(next).then(res=>res),Promise.resolve())
.then(res=>console.log(res))
// 3

Promise.resolve的用法

Promise.reoslve 有一個做用就是能夠將 thenable 對象轉換爲 promise 對象。

thenable 對象,指的是一個具備 .then 方法的對象。
要求是 thenable 對象所擁有的 then 方法應該和 Promise 所擁有的 then 方法具備一樣的功能和處理過程。
一個標準的 thenable 對象應該是這樣的

1 var thenable = {
2   then: function(resolve, reject) {
3     resolve(42);
4   }
5 };

使用 Promise.resolve轉換

Promise.resolve(thenable).then(function(value) {
  console.log(value);  // 42
});

一樣具備標準的thenable特性的是 不一樣的實現Promise標準的類庫,因此 ES6 Promise 與 Q 與buldbird 的對象都是能夠互相轉換的。

jQueyr的defer對象轉換爲ES6 Promise對象

Promise.resolve($.ajax('api/data.json')).then(res=>console.log(res)))

但也不是全部thenable對象都能被成功轉換,主要看各類類庫實現是否遵循 Promise/A+標準,不過此類使用場景並很少,不作深刻討論。

 

最佳實踐

  1. then方法中 永遠 return 或 throw
  2. 若是 promise 鏈中可能出現錯誤,必定添加 catch
  3. 永遠傳遞函數給 then 方法
  4. 不要把 promise 寫成嵌套

通過本篇的對Promise相關知識的理解和學習,基本上對Promise的概念和使用有了比較詳細的瞭解,下一篇就讓咱們一塊兒進入 Promise 的源碼世界看一看吧。

閱讀參考
談談使用 promise 時候的一些反模式

相關文章
相關標籤/搜索