深入理解 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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...