ES6之什麼是Promise?

爲什麼要使用 Promise?

在我們 javaScript 大環境下,我們的編程方式更多是基於異步編程, 究竟什麼是異步編程,爲什麼要異步編程,我們之後的文章會說。在異步 編程中使用的最多的就是回調函數,先了解一下什麼是回調函數。

回調函數指的是:被調用者回頭調用調用者的函數,這種由調用方自己提 供的函數叫回調函數。

應用場景舉例:

對於數組的 filter 方法,裏面實現的過濾的邏輯,實現瞭如何過濾, 但是過濾的條件是什麼,filter 方法中提供回調函數,讓我們自己寫。爲提 高程序的通用性,掉用者提供一個過濾條件函數,這樣 filter 函數借此調 用調用者的函數來進行過濾。

應用實例舉例:

[1,2,3,4,5].filter(item => item % 2 ==0)
//返回數組當中偶數構成的數組。

在前端當中涉及使用回調函數的地方非常的多。最常使用的地方在於我們 發送 Ajax 請求。一個請求發出去我們在等待結果,就會有相應的成功處理 函數,以及失敗處理函數。這裏處理函數指的就是我們的回調函數。同步 回調沒有什麼問題,真的回調問題在於異步,記下來我們一起來看。

// A
ajax( "..", function(..){
    // C
} );
// B

// A 和// B 代表程序的前半部分(也就是 現在),// C 標識了程序的後半 部分(也就是 稍後)。前半部分立即執行,然後會出現一個不知多久的“暫停”。在未來某個時刻,如果 Ajax 調用完成了,那麼程序會回到它剛 才離開的地方,並 繼續 執行後半部分。 換句話說,回調函數包裝或封裝了程序的 延續。 讓我們把代碼弄得更簡單一些:

// A
setTimeout( function(){
    // C
}, 1000 );
// B

稍停片刻然後問你自己,你將如何描述(給一個不那麼懂 JS 工作方式的 人)這個程序的行爲:

 現在大多數同學可能在想或說着這樣的話:“做 A,然後設置一個等待 1000 毫秒的定時器,一旦它觸發,就做 C”。

與你的版本有多接近? 你可能已經發覺了不對勁兒的地方,給了自己一個修正版:“做 A,設置 一個 1000 毫秒的定時器,然後做 B,然後在超時事件觸發後,做 C”。

這 比第一個版本更準確。你能發現不同之處嗎? 雖然第二個版本更準確,但是對於以一種將我們的大腦匹配代碼,代碼匹 配 JS 引擎的方式講解這段代碼來說,這兩個版本都是不足的。這裏的鴻溝 既是微小的也是巨大的,而且是理解回調作爲異步表達和管理的缺點的關 鍵。 只要我們以回調函數的方式引入一個延遲時間(或者像許多程序員那樣引 入幾十個!),我們就允許了一個分歧在我們的大腦如何工作和代碼將運 行的方式之間形成。當這兩者背離時,我們的代碼就不可避免地陷入這樣 的境地:更難理解,更難推理,更難調試,和更難維護。

主要的原因在於 我們的大腦。我們大腦的邏輯,也就是人正常的思維邏輯與這種回調的方 式不符。人更擅長做完一件事,在做另一件事。

接着來在一個實際場景下看看如何編程,以及在書寫代碼過程中會出現怎 麼樣的問題。

問題: 實現網上購票流程。

解決上面的基本流程是:

1. 查詢車票

2. 查詢到車票,進行購買

3. 購買查詢車票,進行佔票

4. 佔票成功後進行付款

5. 付款成功後打印車票

網上購票分爲以上五個步驟,但是這裏面每一步都是需要異步執行的:

$.ajax({ // 發送查詢車票請求
    type: 'GET',
    url: '/api/serachTickey?begin="beijing"&end="Harbin"',
    success: function (req) { // 查詢成功
        renderList(req.data.list) // 渲染列表
        //用戶選擇車次後繼續買票
        $.ajax({
            type: 'GET',
            url:'/api/buyTickey?begin=beijing&end=Harbin&trainNuber=8888',
            success: function (req) { // 搶票成功,接下來進行付款
                $.ajax({
                    type: 'GET',
                    url: `/api/payMent?moeny=${req.money}`,
                    success: function () {
                        console.log('付款失敗成功')
                    },
                    error: function () {
                        console.log('付款失敗')
                    }
                })
            },
            error: function (err) { // 買票失敗
                console.log(err)
            }
        })
    },
    error: function (err) { // 查詢失敗
        console.log(err)
    }
})

這樣的代碼常被稱爲“回調地獄(callback hell)”,有時也被稱爲“末日金字塔(pyramid of doom)”(由於嵌套的縮進使它看起來像一個放倒 的三角形)。 但是“回調地獄”實際上與嵌套/縮進幾乎無關。我們可以把上面的代 碼改寫成:

$.ajax({
    type: 'GET',
    url: '/api/serachTickey?begin="beijing"&end="Harbin"',
    success: buyTicket,
        error: error
})
function buyTicket(req) {
    renderList(req.data.list) // 渲染列表
    //用戶選擇車次後繼續買票
    $.ajax({
        type: 'GET',
        url:'/api/buyTickey?begin=beijing&end=Harbin&trainNuber=8888',
        success: payMent,
        error: error
    })
}
function payMent(req) {
    // 搶票成功,接下來進行付款
    $.ajax({
        type: 'GET',
        url: `/api/payMent?moeny=${req.money}`,
        success: printMsg,
        error: error
    })
}
function printMsg(msg) {
    console.log(msg)
}
function error(err) {
    console.log(err)
}

一樣的代碼組織形式幾乎看不出來有前一種形式的嵌套/縮進困境,但 它的每一處依然容易受到“回調地獄”的影響。爲什麼呢?

當我們線性地(順序地)推理這段代碼,我們不得不從一個函數跳到 下一個函數,再跳到下一個函數,並在代碼中彈來彈去以“看到”順序流。 並且要記住,這個簡化的代碼風格是某種最佳情況。我們都知道真實的 JS 程序代碼經常更加神奇地錯綜複雜,使這樣量級的順序推理更加困難。

另一件需要注意的事是:爲了將第 2,3,4 步鏈接在一起使他們相繼 發生,回調獨自給我們的啓示是將第 2 步硬編碼在第 1 步中,將第 3 步硬 編碼在第 2 步中,將第 4 步硬編碼在第 3 步中,如此繼續。

硬編碼不一定 是一件壞事,如果第 2 步應當總是在第 3 步之前真的是一個固定條件。 不過硬編碼絕對會使代碼變得更脆弱,因爲它不考慮任何可能使在步 驟前行的過程中出現偏差的異常情況。舉個例子,如果第 2 步失敗了,第 3 步永遠不會到達,第 2 步也不會重試,或者移動到一個錯誤處理流程上, 等等。

所有這些問題你都可以手動硬編碼在每一步中,但那樣的代碼總是重 復性的,而且不能在其他步驟或你程序的其他異步流程中複用。

即便我們的大腦可能以順序的方式規劃一系列任務(這個,然後這個, 然後這個),但我們大腦運行的事件的性質,使恢復/重試/分流這樣的流 程控制幾乎毫不費力。如果你出去購物,而且你發現你把購物單忘在家裏 了,這並不會因爲你沒有提前計劃這種情況而結束這一天。你的大腦會很 容易地繞過這個小問題:你回家,取購物單,然後回頭去商店。

但是手動硬編碼的回調(甚至帶有硬編碼的錯誤處理)的脆弱本性通 常不那麼優雅。一旦你最終指明瞭(也就是提前規劃好了)所有各種可能 性/路徑,代碼就會變得如此複雜以至於幾乎不能維護或更新。

這 纔是“回調地獄”想表達的!嵌套/縮進基本上一個餘興表演,盡 管看起來還是有些不方便的。

上面是多個回調配合着嵌套產生的回調地獄問題,回調還會產生信任問題。 在順序的大腦規劃和 JS 代碼中回調驅動的異步處理間的不匹配只是關於 回調的問題的一部分。還有一些更深刻的問題值得擔憂。 讓我們再一次重溫這個概念——回調函數是我們程序的延續(也就是程序 的第二部分):

// A
ajax( "..", function(..){
    // C
} );
// B

// A 和// B 現在 發生,在 JS 主程序的直接控制之下。但是// C 被推遲到 稍後 再發生,並且在另一部分的控制之下——這裏是 ajax(..)函數。在基 本的感覺上,這樣的控制交接一般不會讓程序產生很多問題。

但是不要被這種控制切換不是什麼大事的罕見情況欺騙了。事實上,它是 回調驅動的設計的最可怕的(也是最微妙的)問題。這個問題圍繞着一個 想法展開:有時 ajax(..)(或者說你向之提交回調的部分)不是你寫的函數, 或者不是你可以直接控制的函數。很多時候它是一個由第三方提供的工具。 當你把你程序的一部分拿出來並把它執行的控制權移交給另一個第三方 時,我們稱這種情況爲“控制反轉”。在你的代碼和第三方工具之間有一 個沒有明言的“契約”——一組你期望被維護的東西。

你是一個開發者,正在建造一個販賣昂貴電視的網站的結算系統。你已經 將結算系統的各種頁面順利地製造完成。在最後一個頁面,當用戶點解“確 定”購買電視時,你需要調用一個第三方函數(假如由一個跟蹤分析公司 提供),以便使這筆交易能夠被追蹤。

你注意到它們提供的是某種異步追蹤工具,也許是爲了最佳的性能,這意 味着你需要傳遞一個回調函數。在你傳入的這個程序的延續中,有你最後 的代碼——劃客人的信用卡並顯示一個感謝頁面。 這段代碼可能看起來像這樣:

analytics.trackPurchase( purchaseData, function(){
    chargeCreditCard();
    displayThankyouPage();
} );

足夠簡單,對吧?你寫好代碼,測試它,一切正常,然後你把它部署到生 產環境。大家都很開心!

若干個月過去了,沒有任何問題。你幾乎已經忘了你曾寫過的代碼。一天 早上,工作之前你先在咖啡店坐坐,悠閒地享用着你的拿鐵,直到你接到 老闆慌張的電話要求你立即扔掉咖啡並衝進辦公室。

當你到達時,你發現一位高端客戶爲了買同一臺電視信用卡被劃了 5 次, 而且可以理解,他不高興。客服已經道了歉並開始辦理退款。但你的老闆 要求知道這是怎麼發生的。“我們沒有測試過這樣的情況嗎!?” 你甚至不記得你寫過的代碼了。但你還是往回挖掘試着找出是什麼出錯了。 在分析過一些日誌之後,你得出的結論是,唯一的解釋是分析工具不知怎 麼的,也就是第三方的函數出了問題,由於某些原因,將你的回調函數調 用了 5 次而非一次。他們的文檔中沒有任何東西提到此事。

於是你聯繫了客戶支持,當然他們和你一樣驚訝。他們同意將 此事向上提交至開發者,並許諾給你回覆。第二天,你收到一封很長的郵 件解釋他們發現了什麼,然後你將它轉發給了你的老闆。 看起來,分析公司的開發者曾經制作了一些實驗性的代碼,在一定條件下, 將會每秒重試一次收到的回調,在超時之前共計 5 秒。他們從沒想要把這 部分推到生產環境,但不知怎地他們這樣做了,而且他們感到十分難堪而 且抱歉。然後是許多他們如何定位錯誤的細節,和他們將要如何做以保證 此事不再發生。等等,等等。

你找你的老闆談了此事,但是他對事情的狀態不是感覺特別舒服。他堅持, 而且你也勉強地同意,你不能再相信他們了,而你將需要指出如何保護放 出的代碼,使它們不再受這樣的漏洞威脅。 修修補補之後,你實現了一些如下的特殊邏輯代碼,團隊中的每個人看起 來都挺喜歡:

var tracked = false;
analytics.trackPurchase( purchaseData, function(){
    if (!tracked) {
    tracked = true;
    chargeCreditCard();
        displayThankyouPage();
    }
} );

在這裏我們實質上創建了一個門閥來處理我們的回調被併發調用多次的 情況。 但一個 QA 的工程師問,“如果他們沒調你的回調怎麼辦?” 噢。誰也沒 想過。 你開始佈下天羅地網,考慮在他們調用你的回調時所有出錯的可能性。這 裏是你得到的分析工具可能不正常運行的方式的大致列表:

  • 調用回調過早(在它開始追蹤之前)
  • 調用回調過晚 (或不調)
  • 調用回調太少或太多次(就像你遇到的問題!)
  • 沒能向你的回調傳遞必要的環境/參數 吞掉了
  • 可能發生的錯誤/異常
  • ..

這感覺像是一個麻煩清單,因爲它就是。你可能慢慢開始理解,你將要不 得不爲 每一個傳遞到你不能信任的工具中的回調 都創造一大堆的特殊 邏輯。

在上面的過程中我們發現幾個大問題:

1. 回調配合着嵌套會產生回調地獄問題,思路很不清晰。

2. 由於回調存在着依賴反轉,在使用第三方提供的方法時,存在信任問 題。

3. 當我們不寫錯誤的回調函數時,會存在異常無法捕獲 4. 導致我們的性能更差,本來可以一起做的但是使用回調,導致多件事 情順序執行。用的時間更多

針對這樣的問題我們該怎麼解決呢?

我們首先想要解決的是信任問題,信任是如此脆弱而且是如此的容易 丟失。 回想一下,我們將我們的程序的延續包裝進一個回調函數中,將這個 回調交給另一個團體(甚至是潛在的外部代碼),並雙手合十祈禱它會做 正確的事情並調用這個回調。 我們這麼做是因爲我們想說,“這是 稍後 將要發生的事,在當前的 步驟完成之後。” 但是如果我們能夠反向倒轉這種控制反轉呢?如果不是將我們程序 的延續交給另一個團體,而是希望它返回給我們一個可以知道它何時完成 的能力,然後我們的代碼可以決定下一步做什麼呢?

這種規範被稱爲 Promise。

Promise 正在像風暴一樣席捲 JS 世界,因爲開發者和語言規範作者之 流拼命地想要在他們的代碼/設計中結束回調地獄的瘋狂。事實上,大多數 新被加入 JS/DOM 平臺的異步 API 都是建立在 Promise 之上的。

什麼是 Promise?

Promise 是異步編程的一種解決方案:從語法上講,promise 是一個對象, 從它可以獲取異步操作的消息;從本意上講,它是承諾,承諾它過一段時 間會給你一個結果。promise 有三種狀態:pending(等待態),fulfiled(成功態),rejected(失敗態);狀態一旦改變,就不會再變。

創造 promise 實例後, 它會立即執行。一般來說我們會碰到的回調嵌套都不會很多,一般就一到 兩級,但是某些情況下,回調嵌套很多時,代碼就會非常繁瑣,會給我們 的編程帶來很多的麻煩,這種情況俗稱——回調地獄。 這時候我們的 promise 就應運而生、粉墨登場了。

Promise 的基本使用

Promise 是一個構造函數,自己身上有 all、reject、resolve 這幾個眼熟的方 法,原型上有 then、catch 等同樣很眼熟的方法。

let p = new Promise((resolve, reject) => {
    //做一些異步操作
    setTimeout(() => {
        console.log('執行完成');
        resolve('我是成功!!');
    }, 2000);
});

Promise 的構造函數接收一個參數:函數,並且這個函數需要傳入兩個參 數:

  • resolve :異步操作執行成功後的回調函數
  • reject:異步操作執行失敗後的回調函數

then 鏈式操作的用法

所以,從表面上看,Promise 只是能夠簡化層層回調的寫法,而實質上, Promise 的精髓是“狀態”,用維護狀態、傳遞狀態的方式來使得回調函 數能夠及時調用,它比傳遞 callback 函數要簡單、靈活的多。所以使用 Promise 的正確場景是這樣的,把我們之前的問題修改一下:

function myAjax(url,type="GET") {
    return new Promise((resolve, reject) => {
        $.ajax({
            type,
            url,
            success: resolve,
            error: reject
        })
    })
}

myAjax('/api/serachTickey?begin="beijing"&end="Harbin"')
.then(req => {
    renderList(req.data.list)
    return    myAjax('/api/buyTickey?begin=beijing&end=Harbin&trainNuber=8888')
 },err => { 
    console.log(err)
})
.then(req => {
    return myAjax(`/api/payMent?moeny=${req.money}`)
},err => console.log(err))
.then(req => {
    console.log(req.msg)
}, err => console.log(err))

通過 Promise 這種方式很好的解決了回調地獄問題,使得異步過程同步化, 讓代碼的整體邏輯與大腦的思維邏輯一致,減少出錯率。

reject 的用法 

把 Promise 的狀態置爲 rejected,這樣我們在 then 中就能捕捉到,然後執 行“失敗”情況的回調。看下面的代碼。

let p = new Promise((resolve, reject) => {
    //做一些異步操作
    setTimeout(function(){
        var num = Math.ceil(Math.random()*10); //生成 1-10 的隨機數
        if(num<=5){
            resolve(num);
        }
        else{
            reject('數字太大了');
        }
     }, 2000);
});

p.then((data) => {
    console.log('resolved',data);
},(err) => {
    console.log('rejected',err);
}); 

then 中傳了兩個參數,then 方法可以接受兩個參數,第一個對應 resolve 的回調,第二個對應 reject 的回調。所以我們能夠分別拿到他們傳過來的 數據。多次運行這段代碼,你會隨機得到兩種結果。

catch 的用法

我們知道 Promise 對象除了 then 方法,還有一個 catch 方法,它是做什麼 用的呢?其實它和 then 的第二個參數一樣,用來指定 reject 的回調。用法 是這樣:

p.then((data) => {
    console.log('resolved',data);
}).catch((err) => {
    console.log('rejected',err);
});

效果和寫在 then 的第二個參數裏面一樣。不過它還有另外一個作用:在 執行 resolve 的回調(也就是上面 then 中的第一個參數)時,如果拋出異 常了(代碼出錯了),那麼並不會報錯卡死 js,而是會進到這個 catch 方 法中。請看下面的代碼:

p.then((data) => {
    console.log('resolved',data);
    console.log(somedata); //此處的 somedata 未定義
})
.catch((err) => {
    console.log('rejected',err);
});

在 resolve 的回調中,我們 console.log(somedata);而 somedata 這個變量是 沒有被定義的。如果我們不用 Promise,代碼運行到這裏就直接在控制檯 報錯了,不往下運行了 也就是說進到 catch 方法裏面去了,而且把錯誤原因傳到了 reason 參數中。 即便是有錯誤的代碼也不會報錯了,這與我們的 try/catch 語句有相同的功能。

all 的用法

誰跑的慢,以誰爲準執行回調。all 接收一個數組參數,裏面的 值最終都算返回 Promise 對象 Promise 的 all 方法提供了並行執行異步操作的能力,並且在所有異步操作 執行完後才執行回調。看下面的例子:

let Promise1 = new Promise(function(resolve, reject){})
let Promise2 = new Promise(function(resolve, reject){})
let Promise3 = new Promise(function(resolve, reject){})
let p = Promise.all([Promise1, Promise2, Promise3])
p.then(funciton(){
    // 三個都成功則成功
}, function(){
    // 只要有失敗,則失敗
})

有了 all,你就可以並行執行多個異步操作,並且在一個回調中處理所有的 返回數據,是不是很酷?有一個場景是很適合用這個的,一些遊戲類的素 材比較多的應用,打開網頁時,預先加載需要用到的各種資源如圖片、flash 以及各種靜態文件。所有的都加載完後,我們再進行頁面的初始化。在這 裏可以解決時間性能的問題,我們不需要在把每個異步過程同步出來。

race 的用法

誰跑的快,以誰爲準執行回調 race 的使用場景:比如我們可以用 race 給某個異步請求設置超時時間,並 且在超時後執行相應的操作,代碼如下:

function requestImg(){
    var p = new Promise((resolve, reject) => {
        var img = new Image();
        img.onload = function(){
            resolve(img);
        }
        img.src = '圖片的路徑';
    });
     return p;
 }
//延時函數,用於給請求計時
function timeout(){
    var p = new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('圖片請求超時');
         }, 5000);
    });
    return p;
}
Promise.race([requestImg(), timeout()]).then((data) =>{
    console.log(data);
}).catch((err) => {
    console.log(err);
});

接下來再說一說 Promise 解決回調信任問題:

回顧一下只用回調編碼的信任問題,把一個回調傳入工具 foo()時可能出現 如下問題:

  • 調用回調過早 調用回調過晚(或不被調用)
  • 調用回調次數過少或過多
  • 未能傳遞所需的環境和參數
  • 吞掉可能出現的錯誤和異常

Promise 的特性就是專門用來爲這些問題提供一個有效的可複用的答案。

調用過早:

根據定義,Promise 就不必擔心這種問題,因爲即使是立即完成的 Promise (類似於 new Promise(function(resolve){ resolve(42); }) )也無法被同步觀 察到。 也就是說,對一個 Promise 調用 then()的時候,即使這個 Promise 已經決 議,提供給 then()的回調也總會被異步調用。

調用過晚:

Promise 創建對象調用 resolve()或 reject()時,這個 Promise 的 then()註冊的 觀察回調就會被自動調度。可確信,這些被調度的回調在下一個異步事件 點上一定會被觸發。 同步查看是不可能的,所以一個同步任務鏈無法以這種方式運行來實現按 照預期有效延遲另一個回調的發生。也就是說,一個 Promise 決議後,這 個 Promise 上所有的通過 then()註冊的回調都會在下一個異步時機點上依 次被立即調用。這些回調中的任意一個都無法影響或延誤對其他回調的調 用。

p.then( function(){
    p.then( function(){
        console.log( "C" );
    });
    console.log( "A" );
} );
p.then( function(){
    console.log( "B" );
});
// A B C

這裏,“C” 無法打斷或搶佔“B”,這是因爲 Promise 的運作方式。

Promise 調度技巧

有很多調度的細微差別。這種情況下,兩個獨立 Promise 上鍊接的回調的 相對順序無法可靠預測。 如果兩個 Promise p1 和 p2 都已經決議,那麼 p1.then(), p2.then()應該 最終會制調用 p1 的回調,然後是 p2。但還有一些微妙的場景可能不是這 樣。

var p3 = new Promise( function(resolve, reject){
    resolve( "B" );
});
var p1 = new Promise(function(resolve, reject){
    resolve( p3 );
})
p2 = new Promise(function(resolve, reject){
    resolve( "A" );
})
p1.then( function(v){
    console.log( v );
})
p2.then( function(v){
    console.log( v );
})
// A B , 而不是像你認爲的 B A

p1 不是用立即值而是用另一個 promise p3 決議,後者本身決議爲值“B”。 規定的行爲是把 p3 展開到 p1,但是是異步地展開。所以,在異步任務隊列 中,p1 的回調排在 p2 的回調之後。 要避免這樣的細微區別帶來的噩夢,你永遠都不應該依賴於不同 Promise間回調的順序和調度。實際上,好的編碼實踐方案根本不會讓多個回調的 順序有絲毫影響,可能的話就要避免。

回調未調用

首先,沒有任何東西(甚至 JS 錯誤)能阻止 Prmise 向你通知它的決議(如 果它決議了的話)。如果你對一個 Promise 註冊了一個完成回調和一個拒 絕回調,那麼 Promise 在決議時總是會調用其中一個。

當然,如果你的回調函數本身包含 JS 錯誤,那可能就會看不到你期望的結 果。但實際上回調還是被調用了。後面討論,這些錯誤並不會被吞掉。 但是,如果 Promise 永遠不決議呢?即使這樣,Promise 也提供瞭解決方 案。其使用了一種稱爲竟態的高級抽象機制:

// 用於超時一個 Promise 的工具
function timeoutPromise(delay){
    return new Promise( function(resolve, reject){
        setTimeout(function(){
            reject("Timeout!");
        }, delay)
    })
}    
    // 設置 foo()超時    
Promise.race( [
    foo(),
    timeoutPromise( 3000 );
]).then( function(){
    // foo() 及時完成!
},function(err){
    // 或者 foo()被拒絕,或者只是沒能按時完成
    // 查看 err 來了解是哪種情況
})

我們可保證一個 foo()有一個信號,防止其永久掛住程序。

調用次數過少或過多:

根據定義,回調被調用的正確次數應該是 1。“過少”的情況就是調用 0 次,和前面解釋過的“未被”調用是同一種情況。

“過多”容易解釋。Promise 的定義方式使得它只能被決議一次。如果出 於某種原因,Promise 創建代碼試圖調用 resolve()或 reject()多次,或者試 圖兩者都調用,那麼這個 Promise 將只會接受第一次決議,並默默地忽略 任何後續調用。 由於 Promise 只能被決議一次,所以任何通過 then()註冊的(每個)回調 就只會被調用一次。 當然,如果你把同一個回調註冊了不止一次(比如 p.then(f); p.then(f)), 那頭被調用的次數就會和註冊次數相同。響應函數只會被調用一次。

未能傳遞參數/環境值:

Promise 至多隻能有一個決議值(完成或拒絕)。 如果你沒有用任何值顯式決議,那麼這個值就是 undefined,這是 JS 常見 的處理方式。但不管這個值是什麼,無論當前或未來,它都會被傳給所有 註冊的(且適當的完成或拒絕)回調。 還有一點需要清楚:如果使用多個參數調用 resovel()或者 reject()第一個參 數之後的所有參數都會被默默忽略。 如果要傳遞多個值,你就必須要把它們封裝在一個數組或對象中。 對環境來說,JS 中的函數總是保持其定義所在的作用域的閉包,所以它們 當然可繼續你提供的環境狀態。

吞掉錯誤或異常:

如果在 Promise 的創建過程中或在查看其決議結果過程中的任何時間點上 出現了一個 JS 異常錯誤,比如一個 TypeError 或 RefernceError,那這個異 常就會被捕捉,並且會使這個 Promise 被拒絕。

var p = new Promise( function(resolve, reject){
    foo.bar(); // foo 未定義,所以會出錯
    resolve(42); // 永遠不會到達這裏
});
p.then(function fulfilled(){
    // 永遠不會到這裏
},function rejected(err){
    // err 將會是一個 TypeError 異常對象來自 foo.bar()這一行
})

foo.bar()中發生的JS異常導致了Promise拒絕,你可捕捉並對其做出響應。 Promise 甚至把 JS 異常也變成了異步行爲,進而極大降低了竟態條件出現 的可能。 但是,如果 Promise 完成後在查看結果時(then()註冊回調中)出現了 JS 異常錯誤會怎樣呢?

var p = new Promise( function(resolve, reject){
    resolve( 42 );
});
p.then(function fulfilled(msg){
    foo.bar();
    console.log( msg ); // 永遠不會到達這裏
},function rejected(err){
    // 永遠也不會到達這裏
})

等一下,這看 qvnn 來像是 foo.bar()產生的異常真的被吞掉了。別擔心,實 際上並不是這樣。但是這裏有一個深的問題。就是我們沒有偵聽到它。 p.then()調用本身返回了另一個 promise,正是這個 promise 將會因 TypeError 異常而被拒絕。

Promise可信任嘛?

你肯定已經注意到 Promise 並沒有完全擺脫回調。它們只是改變了傳遞迴 調的位置。我們並不是把回調傳遞給 foo(),而是從 foo()得到某個東西, 然後把回調傳給這個東西。 但是,爲什麼這就比單純使用回調更值得信任呢?

關於 Promise 的很重要但是常常被忽略的一個細節是,Promise 對這個問 題已經有一個解決方案。包含在原生 ES6 Promise 實現中的解決方案就是 Promise.resolve()。

如果向 Promise.resolve()傳遞一個非 Promise、非 thenable 的立即值,就會 得到一個用這個值填充的 promise。下面這種情況下,promise p1 和 promise p2 的行爲是完全一樣的:

var p1 = new Promise( function(resolve, reject){
    resolve( 42 );
})
var p2 = Promise.resolve(42);

而如果向 Promise.resolve() 傳遞一個真正的 Promise,就只會返回同一個:

promise
var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( p1 );
p1 === p2; // true

如果向 Promise.resolve()傳遞了一個非 Promise 的 thenable 值,前者會試 圖展開這個值,而且展開過程會持續到提取出一個具體的非類 Promise 的 最終值。

var p = {
    then: function(cb){
        cb( 42 );
    }
};
// 這可以工作,但只是因爲幸運而已
p.then(function fulfilled(val){
    console.log( val ); //42
},function rejected(err){
    // 永遠不會到這裏
})

但是,下面又會怎樣呢?

var p = {
    then: function(cb, errcb){
        cb(42);
        errcb("evil laugh");
    }
};
p.then(function fulfilled(val){
    console.log( val ); //42
},function rejected(err){
    // 啊,不應該運行!
    console.log( err ); // 邪惡的笑
})

儘管如此,我們還是都可把這些版本的 p 傳給 Promise.resolve(),然後就 會得到期望中的規範化後的安全結果:

Promise.resolve(p)
.then(function fulfilled(val){
    console.log(val); //42
},function rejected(err){
    // 永遠不會到這裏
})

Promise.resolve()可接受任何 thenable,將其解封完它的非 thenable 值。從 Promise.resolve()得到的是一個真正的 Promise,是一個可信任的值。如果你 傳入的已經是真正的 Promise,那們你得到的就是它本身,所以通過 Promise.resolve()過濾來獲得可信任性完全沒有壞處。

假設我們要調用一個工具 foo(),且不確定得到的返回值是否是一個可信任 的 行 爲 良 好 的 Promise , 但 我 們 可 知 道 它 至 少 是 一 個 thenable 。 Promise.resolve()提供了可信任的 Promise 封裝工具,可鏈接使用:

// 不要這麼做
foo(42).then(function(v) {
    console.log( v );
});
// 而要這麼做
Promise.resolve( foo(42) ).then( function(v){
    console.log(v)
})

對於用Promise.resolve() 爲所有函數的返回值都封裝一層。另一個好處是, 這樣做很容易把函數調用規範爲定義良好的異步任務。如果 foo(42)有時會 返回一個立即值,有時會返回 Promise,那麼 Promise.resolve(foo(42))就能 保證總返回一個 Promise 結果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章