你不知道的Promise

摘自你不知道的JavaScript中卷

前言

很多人都知道JS中的回調(diào)地獄(毀滅金字塔)一說,認為回調(diào)帶來的缺陷就是那一層又一層的嵌套和縮進,但實際上回調(diào)地獄與嵌套,縮進幾乎沒有關系,回調(diào)所引起的問題要遠遠比這二者嚴重的多,下面將一一分析

回調(diào)地獄

代碼示例

舉一個書中的關于嵌套的回調(diào)代碼示例

listen( 'click', function handler(evt) {
  setTimeout( function request () {
      ajax( 'http://some.url.1', function response (text) {
          if (test == 'hello') {
              handler();
          }else if (text == 'world') {
              request();
          }
      });
  }, 500);
});

以上示例就是人們口中常說的回調(diào)地獄,下面描述一下該代碼示例所大致的執(zhí)行流程

  1. 等待一個click事件

  2. 當click事件發(fā)生時,等待定時器500ms

  3. 等待Ajax相應返回,之后當返回值為'world'時,可能會重新請求

不得不說,為了看懂以上代碼示例,我們的大腦需要努力的分析才能知道整個異步流程執(zhí)行的順序是什么,這便是我所說的關于第一個回調(diào)帶來的缺陷,即:難以理解代碼意圖

信任問題

書中舉了一個很好的關于回調(diào)帶來的信任問題,我們知道關于JavaScript中異步回調(diào)的代碼將會在將來的某個時刻由第三方回調(diào)我們,那既然是由第三方回調(diào)我們,就會帶來一些隱藏的嚴重的問題,它什么時候回調(diào)?會不會出現(xiàn)過早/過晚回調(diào)?它會回調(diào)多次嗎?它會不會干脆不回調(diào)我們等等

而出現(xiàn)這一系列不確定問題的關鍵則在于我們將自己程序中一部分代碼的執(zhí)行控制權交給了某個第三方,可能是函數(shù)ajax,也肯能是你對接的第三方客戶等

五個回調(diào)的故事

書中描述了一個關于五個回調(diào)的小故事,大概的意思是這樣的,某售賣昂貴電視的公司有一個在線售賣電視的網(wǎng)站,該網(wǎng)站有一個功能是當用戶點擊 '確認' 購買電視時,網(wǎng)站腳本需要調(diào)用某第三方公司提供的用于跟蹤該比交易的函數(shù),而該第三方公司為了提高調(diào)用性能,采用了異步回調(diào)的形式,即網(wǎng)站腳本在調(diào)用第三方函數(shù)時需要傳入一個用于回調(diào)的函數(shù),真正出問題的就在這個用于回調(diào)的函數(shù)中,網(wǎng)站開發(fā)人員在回調(diào)函數(shù)中去收取客戶費用和展示感謝購買的頁面

然而某一天出現(xiàn)了一個嚴重的線上問題,某客戶在購買電視時費用被扣除了五次,造成了嚴重的生產(chǎn)事故

而最終的原因是第三方連續(xù)調(diào)用了五次回調(diào)函數(shù),造成客戶被扣款五次,很多人可能會說,這應該是你自己的問題,是你沒有做好回調(diào)函數(shù)的冪等,沒有做多次調(diào)用的處理

那么如果我們仔細的分析以上的小故事,就會發(fā)現(xiàn),這就是回調(diào)交給外部代碼所帶來的嚴重缺陷,既然是回調(diào),就會存在很多的情況,比如:回調(diào)過早,過晚,次數(shù)太多,太少,不回調(diào)等等


如何挽救回調(diào)

如果我們不把自己程序的將來部分(continuation)傳給第三方,而是希望第三方給我們提供一種了解其任務何時結束的能力,然后由我們自己的代碼來決定下一步做什么

這種范式就稱為 Promise

Promise 承諾

分析Promise如何解決回調(diào)引起的信任問題之前先回顧一下回調(diào)可能引起的信任問題有哪些

  • 調(diào)用過早

  • 調(diào)用過晚或不調(diào)用

  • 調(diào)用回調(diào)次數(shù)過多或過少

  • 吞掉可能出現(xiàn)的錯誤和異常

  • ...

下面一一分析

調(diào)用過早

即使是立即完成的Promise,也無法被同步觀察到,也就是說對一個Promise調(diào)用then(...)的時候,即使該Promise已經(jīng)決議,提供給then(...)回調(diào)也總會被異步調(diào)用

調(diào)用過晚

當Promise創(chuàng)建對象調(diào)用resolve(...)或reject(...)時,該promise的then(...)注冊的觀察回調(diào)就會被自動調(diào)度,可以確信,這些被調(diào)度的回調(diào)在下一個異步事件點上一定會被觸發(fā)

回調(diào)未調(diào)用

首先,沒有任何東西(甚至是JavaScript 錯誤)能阻止Promise向你通知它的決議

調(diào)用次數(shù)過多或過少

由于Promise只能被決議一次,所以任何通過then(...)注冊的(每一個)回調(diào)就只會被調(diào)用一次

吞掉錯誤或者異常

如果在Promise的創(chuàng)建過程中或在查看其決議結果過程中的任何時間點上出現(xiàn)了一個JavaScript 異常錯誤,比如一個TypeError 或 ReferenceError 那這個異常就會被捕獲,并且會使這個Promise被拒絕(決議失?。?/p>

舉個書中的例子

var p = new Promise((resolve, error) => {
    foo.bar();
    resolve(1);
});

p.then((value) => {
    console.log(value);
}, (error) => {
    console.log('異常: ' + error);
});

// 結果值
異常: ReferenceError: foo is not defined

如果Promise完成決議后再其then(...) 回調(diào)中出現(xiàn)了JavaScript異常錯誤會怎么樣呢?會被吞掉嗎?

下面看一個例子

var p = new Promise((resolve, reject) => {
    resolve(1);
});

p.then(
    function fulfilled(msg) {
        foo.bar();
        console.log(msg);// 代碼會執(zhí)行到這里嗎?不會
    },
    function rejected (error) {
        console.log('異常:' + error);// 代碼會執(zhí)行到這里嗎?不會
    }
);

以上代碼貌似會被吞掉,其實不然,如果我們這樣改一下代碼

var p = new Promise((resolve, reject) => {
    resolve(1);
});

var pp = p.then(
    function fulfilled(msg) {
        foo.bar();
        console.log(msg);// 代碼會執(zhí)行到這里嗎?不會
    },
    function rejected (error) {
        console.log('異常:' + error);// 代碼會執(zhí)行到這里嗎?不會
    }
);
pp.then(
    null, 
    (error) => {
        console.log('異常:' + error);// 代碼執(zhí)行到了這里
    }
);

通過以上示例我們發(fā)現(xiàn),如果在then(...)回調(diào)中發(fā)生了異常,看似異常信息會被吞掉,其實異常是導致了then(...)本身返回的另外一個promise對象的拒絕,只要在新的promise對象的then方法中去捕獲就可以了

相信通過以上的分析,你已經(jīng)主要到Promise并沒有完全擺脫回調(diào),當promise決議后,還是會回調(diào)then(...)指定的回調(diào)函數(shù),也就是說我們并沒有把回調(diào)函數(shù)傳給第三方,而是我們自己編寫的then函數(shù),相反我們從第三方得到了某個東西(Promise承諾),然后把回調(diào)傳給這個承諾

這個Promise可信任嗎?

如何能夠確定返回的這個東西實際上就是一個可信任的Promise呢?為什么這就比使用單純的回調(diào)更值得信任呢?

Promise.resolve()

關于Promise的很重要但也是常被忽略的一個細節(jié)是:Promise的可信任的解決方案就是Promise.resolve(...)

如果向Promise.resolve(...)傳遞一個非Promise,非thenable的立即值,就會得到用這個值填充的promise,如果傳遞的就是一個真正的Promise,那么得到的就是它本身

代碼示例:

var p1 = new Promise((resolve, error) => {
    resolve(42);
});

var p2 = Promise.resolve(42);

var p3 = Promise.resolve(42);

var p4 = Promise.resolve(p3);

p3 === p4;// true

通過Promise.resolve(...)可以定義一個規(guī)范良好的異步任務,同時也可以保證返回值是一個可信任的行為良好的Promise


Promise 鏈式流

關于Promise鏈式流模型只需要記住兩點即可

  • 每次你對Promise調(diào)用then(...),它都會創(chuàng)建并返回一個新的Promise,我們可以將其鏈接起來

  • 不管從then(...)調(diào)用的完成回調(diào)(第一個參數(shù))返回的值是什么,它都會被自動設置為被鏈接Promise(第一個點中的)的完成

這里舉個例子來說明Promise鏈式流

var p = Promise.resolve(21);
var p2 = p.then(v => {
    console.log(v);
    return v * 2;
});

p2.then(v => {
    console.log(v);
});

// 結果值
21
42

可以看到p.then 創(chuàng)建了一個新的Promise,其返回值成為了新Promise的決議值

如果我們在p.then中引入異步呢?其會帶來什么影響呢?

var p = Promise.resolve(21);
var p2 = p.then(v => {
    console.log(v);
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(v * 2);
        }, 1000);
    });
});

p2.then(v => {
    console.log(v);
});

// 結果值
21
...等待1000ms后
42

我們構建的Promise鏈不僅是一個表達多步異步序列的流程控制,更是從一個步驟到下一個步驟傳遞消息的通道

鏈中出現(xiàn)異常

默認情況下,如果你的promise.then(...)沒有指定拒絕處理函數(shù),那么一個默認的拒絕處理函數(shù)就會頂替,而默認的拒絕處理函數(shù)就是將錯誤信息重新拋出,直到遇到顯示定義的拒絕處理函數(shù)為止

默認完成處理函數(shù)

如果then函數(shù)沒有指定完成處理函數(shù)的話,即傳null,那默認的完成處理函數(shù)會頂替,并將接收到的任何值傳遞給下一個步驟的Promise

then(null, error => {
    ...
})
這個模式,雖然表面上只是處理了拒絕,但是實際上還是會把完成值傳遞下去

看個例子

var p = Promise.resolve(21);

var p2 = p.then(null, error => {
    console.log(error);
});

p2.then(v => {
    console.log('p2: ' + v);
});

// 結果值
p2: 21

這里可以看出,雖然p.then(...)表面上沒有顯示傳遞完成值,但是配p2的完成處理函數(shù)仍然獲取到了p的完成值

優(yōu)雅捕獲鏈式流錯誤

前文我們知道在鏈式流Promise中,如果then(...)中拋出異常信息(非決議前拋出的異常),這些異常信息需要在下一個新的Promise中才能處理,那么循環(huán)往復,就需要在最后一次的then(...)之后調(diào)用Promise API catch一下,但是如果在我們Promise鏈末尾的catch內(nèi)部有錯誤怎么辦?

我們沒有捕獲這個最終Promise的結果,也沒有為其注冊拒絕處理函數(shù),異常是不是就會被吞掉了?

書中建議采用defer的方式處理,感興趣同學的可以自己研究下


Promise 模式

Promise.all([...]) 門模式

在異步序列Promise鏈中,任意時刻執(zhí)行有一個異步任務正在執(zhí)行,但是如果我們想要同時執(zhí)行兩個或更多步驟,要怎么實現(xiàn)呢?

考慮這樣一種場景,如果我需要同時發(fā)送兩個Ajax請求,但他們不管誰先完成,你都不關心,只要他們兩個都成功完成,你在去請求第三個Ajax請求

Promise.all([p1, p2])

其中p1,p2通常是Promise的實例,也可以是立即值或thenable,因為進過all方法之后,每一個參數(shù)都會被Promise.resolve(...)過濾,規(guī)范化為真正的Promise實例

Promise.all(...)的返回值是一個主Promise實例,其完成注冊函數(shù)的參數(shù)是其傳入的每一個Promise的完成消息組成的數(shù)組,這里注意一下的是完成消息的數(shù)組順序與指定的順序有關,與每個Promise完成的順序無關

特性:

  • 所有傳入的Promise都決議完成,主Promise才會決議完成

  • 若其中任何一個Promise決議拒絕,主Promise會立即決議拒絕,并丟棄來自其他所有Promise的全部結果

  • 永遠要記住為每一個傳入的Promise關聯(lián)一個拒絕/錯誤處理函數(shù),尤其是從Promise.all([...])返回的那個

Promise.race([...]) 競態(tài)模式

如果你正在觀看一場110米跨欄比賽,那么你只會關注那第一個跨過終點線的劉翔,其余選手在你眼里可能并不重要

Promise.race([p1, p2])

由于只有一個Promise可以取勝,所以完成值是單個消息,并不是一個數(shù)組

特性:

  • 一旦有任何一個Promise決議完成,Promise.race([...])就會完成

  • 一旦有任何一個Promise決議拒絕,Promise.race([...])就會拒絕

即Promise.race([...])決議取決于傳入的第一個決議的Promise


Promise API 簡述

new Promise(...)構造器

構造函數(shù)必須傳入一個函數(shù)回調(diào),該函數(shù)回調(diào)是同步的或立即調(diào)用的,該函數(shù)有兩個參數(shù)(函數(shù)回調(diào)),用以支持Promise的決議,通常這兩個函數(shù)被稱為resolve(...) 和 reject(...)

這里需要注意一點的是resolve(...) 既可能決議完成,也可能決議拒絕,而reject(...)則只是決議拒絕

  • 如果傳給resolve(...)的是一個thenable值,這個值就會被遞歸展開,要構造的Promise則會取用其最終的決議值或狀態(tài),這也是為什么說resolve(...)可能決議完成或拒絕

  • 如果傳給resolve(...)的是一個真正的Promise,直接返回,什么也不做

  • 如果傳給resolve(...)的值是一個非Promise,非thenable的立即值,要構造的Promise就會用這個值決議完成

下面舉一個例子

var fulfilled = {
    then: (cb) => {
        cb(42);
    }
}

var rejected = {
    then: (cb, errCb) => {
        errCb(42);
    }
}

var p1 = Promise.resolve(fulfilled);
var p2 = Promise.resolve(rejected);

// 結果值
// p1 Promise {<resolved>: 42}
// p2 Promise {<rejected>: 42}

以上就是傳給resolve(...)一個thenable值,構造的Promise的狀態(tài)就是采用傳入的thenable值的最終決議值和狀態(tài)的

then() 和 catch()

每一個Promise實例都有then(...)和catch(...)方法,用來注冊決議完成和決議拒絕后的回調(diào)處理函數(shù)

我們知道當Promise決議之后,總是會立即異步的方式調(diào)用這兩個處理函數(shù)之一

  • then(...)

接受一個或兩個參數(shù),第一個用于完成完成回調(diào),第二個用于拒絕回調(diào)

若兩者中的任何一個省略或作為非函數(shù)類型值傳入的話,就會被替換為相應的默認回調(diào)

默認完成回調(diào):把決議完成消息傳遞下去

默認拒絕回調(diào):重新拋出其接收到的出錯原因

  • catch(...)

只接受一個拒絕回調(diào)函數(shù)作為參數(shù),并自動替換默認完成回調(diào)

等價于 then(null, ...)

  • 相同點
  1. 都會創(chuàng)建并返回一個新的Promise實例,用于實現(xiàn)Promise鏈式控制流

  2. 如果在完成或拒絕回調(diào)中發(fā)生異常,那么會導致新生成并返回的Promise是被決議拒絕的

  3. 如果任意一個回調(diào)返回非Promise,非thenable的立即值,該值會被用作新生成并返回的Promise的完成值,注意是決議完成狀態(tài)的值

  4. 如果任意一個回調(diào)返回Promise,thenable,那么該值會被遞歸展開,最終新生成并返回的Promise會采用展開的決議值


總結

  • Promise解決了我們因只用回調(diào)的代碼而備受困擾的控制反轉的問題

  • Promise并沒有擯棄回調(diào),只是把回調(diào)的安排轉交給了一個位于我們和其他工具(第三方)之間的可信任的中介機制

  • 提供了以順序的方式表達異步流的更好的辦法,讓代碼更清晰易懂

以上就是我對Promise的理解,記錄下來,每天進步一點點

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容