深入理解 Promise

来源:http://www.cnblogs.com/hzh-fe/archive/2017/12/21/8081146.html
-Advertisement-
Play Games

自從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 有很多共通的內容。

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

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

 

ES6 Promise API

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

這裡只對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+標準,不過此類使用場景並不多,不做深入討論。

 

最佳實踐

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

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

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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 申請 Let’s Encrypt證書的原因: 現在阿裡雲等都有免費的 https 證書,為什麼還要申請這個呢(估計也是因為阿裡雲這些有免費證書的原因,所以 Let’s Encrypt 知道的人其實並不算太多)? 原因是公司最近接了很多微信小程式的單子,而小程式是必須要 https 的,申請了幾個後阿 ...
  • 1 導入之前先修改工程下相關文件 1.1 只需修改如下三個地方1.2 修改build.gradle文件 1.3 修改gradle/wrapper/gradle-wrapper.properties 1.4 修改app/build.gradle 2 導入修改後的工程 2.1 選擇File|New|Im ...
  • 這裡以自定義一個可以控制圓角顯示的ImageView控制項UpRoundImageView為例,展開說明。 1、/res/values/arrrs.xml 其中declare-styleable標簽的屬性name最好是自定義控制項的類名。 標簽attr的屬性name是自定義的;format:類型值,有多 ...
  • <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> </head> <body> </body> </html> <!DOCTYPE html> <html> <head> <meta charset="UTF-8 ...
  • Chrome Extension是什麼呢?簡而言之,就是Chrome擴展,它是基於Chrome瀏覽器的,我們可以理解它為一個獨立運行在Chrome瀏覽器下的APP,當然核心編程語言就是JavaScript咯,然後結合HTML以及CSS來開發。重點是,這個“APP”功能強大,可以獨自運行,亦可以與打開... ...
  • 在DOM操作里,createElement是創建一個新的節點,createDocumentFragment是創建一個文檔片段。 網上可以搜到的大部分都是說使用createDocumentFragment主要是因為避免因createElement多次添加到document.body引起的效率問題,比如 ...
  • 在我們平時做的很多網站項目中都會需要繪製各種各樣的二維矢量圖形。比如做城市地下管網的斷面圖、管線管點的坐標位置矢量標識圖、鑽孔位置或地層剖面圖等等。我們有很多種方法來繪製這些矢量圖(vml、canvas、svg等等),下麵我要介紹的是SVG繪圖語言,也是我在做項目中用到比較多的,僅以我的個人實戰經驗 ...
  • zTree 優秀的jquery樹插件,文檔詳細,渲染快 使用方法: 1、引用zTree的js和css文件 2、ztree的html為 需加Class:ztree; 3、初始化樹 後臺介面返回數據示例: 4、加入滑鼠移動到顯示的自定義按鈕 5、文檔地址 http://www.treejs.cn/v3/ ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...