自從ES6流行起來,Promise 的使用變得更頻繁更廣泛了,比如非同步請求一般返回一個 Promise 對象,Generator 中 yield 後面一般跟 Promise 對象,ES7中 Async 函數中 await 後面一般也是 Promise 對象,還有更多的 NodeAPI 也會返回 Pr ...
自從ES6流行起來,Promise 的使用變得更頻繁更廣泛了,比如非同步請求一般返回一個 Promise 對象,Generator 中 yield 後面一般跟 Promise 對象,ES7中 Async 函數中 await 後面一般也是 Promise 對象,還有更多的 NodeAPI 也會返回 Promise 對象,可以說現在的編程中 Promise 的使用無處不在,那麼我們是否真的弄懂了 Promise 呢?是否有誤用或錯誤使用 Promise 呢?是否知道 Promise 的實現原理和 Promise 的花樣玩法呢?下麵讓我們一起來探討一下吧。
Promise 規範
這裡只列舉規範中的大致內容,詳細內容請查看 Promises/A+ 中文 ,這是ES6 Promises的前身,是一個社區規範,它和 ES6 Promises 有很多共通的內容。
- 狀態
Promise
的初始狀態是Pending
,狀態只能被轉換為(Resolved)Fulfilled
或Rejected
,狀態的轉換不可逆。 - then 必須有
then
方法,接收兩個可選函數參數onFulfilled
、onRejected
,then
方法必須返回一個新的Promise
對象,為了保證then
中回調的執行順序,回調必須使用非同步執行。 - 相容 不同的
Promise
的實現必須可以互相調用
具體標準的實現將在 中篇 - 手動封裝 中詳細說明
ES6 Promise API
如果你對 Promise的使用 還不是很瞭解,可參考閱讀以下資料:
- promises-book(非常推薦大家閱讀的資料,對Promise講解十分細緻詳盡)
- 阮一峰的Promise科普文
- ES6筆記 - Promise模式(我自己閱讀《ECMAScript 6 入門》的筆記)
這裡只對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里規定的方法。
calvinmetcalf/lie 非常簡潔的 promise 庫,中篇中的手動封裝實現就是參考了這個庫
jakearchibald/es6-promise 相容 Promises/A+ 的類庫, 它只是 RSVP.js 的一個子集,只實現了Promises 規定的 API。
yahoo/ypromise 這是一個獨立版本的 YUI 的 Promise Polyfill,具有和 ES6 Promises 的相容性
Promise擴展類庫
Promise擴展類庫除了實現了Promise中定義的規範之外,還增加了自己獨自定義的功能。
kriskowal/q 類庫 Q 實現了 Promises 和 Deferreds 等規範。 它自2009年開始開發,還提供了面向Node.js的文件IO API Q-IO 等, 是一個在很多場景下都能用得到的類庫。
petkaantonov/bluebird這個類庫除了相容 Promise 規範之外,還擴展了取消promise對象的運行,取得promise的運行進度,以及錯誤處理的擴展檢測等非常豐富的功能,此外它在實現上還在性能問題下了很大的功夫。
註意
在項目中,有可能兩個不同的模塊使用的是兩個不同的Promise類庫,那麼在大部分的Promise的實現中,都是遵循 Promise/A+ 標準和相容ES6 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+標準,不過此類使用場景並不多,不做深入討論。
最佳實踐
then
方法中 永遠return
或throw
- 如果
promise
鏈中可能出現錯誤,一定添加catch
- 永遠傳遞函數給
then
方法 - 不要把
promise
寫成嵌套
經過本篇的對Promise相關知識的理解和學習,基本上對Promise的概念和使用有了比較詳細的瞭解,下一篇就讓我們一起進入 Promise 的源碼世界看一看吧。