【筆記】你不知道的JS讀書筆記——Promise

寫在前面:
Promise這一章的順序對於未接觸過使用過Promise的童鞋而言略抽象了,前邊幾章主要爲了說明Promise和以前的異步方式相比有什麼優點和它能解決什麼問題,後邊才詳解Promise的API設計和各類場景下如何使用Promise。node

建議先了解和簡單使用過Promise後再閱讀,效果更佳。git

正文github

3.1 什麼是Promise

以前的方式:api

  • 利用回調函數封裝程序中的continuation
  • 回調交給第三方
  • 第三方調用回調
  • 實現正確功能

Promise方式:
第三方提供瞭解其任務什麼時候結束的能力數組

Promise的異步特性是基於任務的(圖示以下)
任務隊列.pngpromise

一種處理異步的思路:爲了統一如今和未來,把它們都變成未來,即全部操做都成了異步的瀏覽器

書中關於Promise是個啥的觀點:安全

一種封裝和組合將來值的易於複用的機制
一種在異步任務中做爲兩個或更多步驟的流程控制機制,時序上的this-then-that —— 關注點分離

Promise設計的重要基礎併發

  • Promise必定是異步執行的,即便是當即完成的Promise(相似 new Promise((resolve)=>{ resolve(42) })),也沒法被同步觀察到
  • 一旦Promise決議,它就永遠保持在這個狀態,變成了不變值(immuatable value),這是設計中最基礎和最重要的因素
  • Promise至多隻能有一個決議值(一個!一個!一個!)

引伸:異步

  • Promise的決議結果能夠給多方屢次查看
  • 安全、可靠

3.2 Promise的檢測

基於thenable的鴨子類型

if(
  p !== null && 
  (
    typeof p === 'object' ||
    typeof p === 'function'
  ) && 
  typeof p.then === 'function'
) {
  // 假定這是一個thenable
}
else {
  // 不是thenable
}

這種方式顯然是有些問題的,可是目前通用的方式

3.3 Promise如何解決信任問題

信任問題見 異步篇

3.3.1 調用過早

避免Zalgo這類反作用:一個任務有時同步完成,有時異步完成,可能致使競態條件

Promise從定義上保證了不會存在這種問題:參考3.1 設計基礎 — 即便是當即完成的Promise,也沒法被同步觀察到

3.3.2 調用過晚

Note: 調用過晚強調的是調用順序?

Promise建立對象調用resolve(..)或reject(..)時,這個Promise的then註冊的觀察回調就會自動調度(注意是被調度而不是執行) —— 在下一個異步時機點上依次被調用執行,它們相互之間是不會互相影響或延誤的

3.3.3 回調未調用

Promise一旦決議則必定會通知決議(傳入then的完成回調或拒絕回調調用),即便是Javascript運行錯誤也會調用拒絕回調

若是某個Promise一直不決議呢?使用競態的高級抽象機制:

// 超時工具
function timeoutPromise(delay){
  return new Promise( (resolve, reject) => {
    setTimeout( function () {
      reject('Timeout!');
    }, delay);
  } )
}

// 設置某個Promise foo()超時
Promise.race( [
  foo(),
  timeoutPromise(3000)
] )
.then(
  function () {
    // foo(..)及時完成
  },
  function (err) {
    // foo(..)被拒絕或者超時
    // 經過查看err肯定錯誤狀況
  }
);

3.3.4 調用次數過少或過多

若是建立Promise的代碼試圖屢次調用resolve(..)或reject(..),或者二者都調用,Promise只會接受第一次決議,後續調用都會被忽略

3.3.5 未能傳遞參數/環境值

Promise至多隻能有一個決議值

若是使用多個參數調用resolve(..)或reject(..),第一個參數以後的全部參數都會被忽略

Promise其實也是傳入回調函數,故函數中照樣能根據做用域規則訪問到對應的環境數據

3.3.6 吞掉錯誤或異常

這裏說的錯誤或異常可能出如今兩個過程:

  1. Promise建立過程或其決議確認以前的任什麼時候間點上(注:書中原文查看其決議結果過程當中任什麼時候間點,我的認爲可能翻譯得有點問題,應該要強調是其決議以前)
  2. Promise決議確認後在查看結果時(then(..)註冊的回調中)出現了js異常錯誤

這兩種錯誤都不會被丟棄,但針對它們的處理方式有所不一樣:

針對1:
該Promise會被當即拒絕,但注意這個異常也被變成了異步行爲

let p = new Promise ( function(resolve, reject){
    foo.bar(); // foo undefined 將拋出錯誤 Promise=>reject
    resolve( 42 ); // 不會執行到這裏
});
p.then(
    function fulfilled(){
        // 不會執行到這裏
    },
    function rejected(err){
        // err是一個TypeError異常
    }
)

針對2:
這個時候當前Promise已經決議,其決議結果是個不可變值
then(..)調用返回的下一個Promise被拒絕

let q = new Promise ( function(resolve, reject){
    resolve( 42 );
})
q.then(
    function fulfilled(){
        foo.bar(); // foo undefined 將拋出錯誤 致使then返回的Promise被reject
    },
    function rejected(err){
        // 不會執行到這裏
    }
).then(
    function fulfilled(){
        // 不會執行到這裏
    },
    function rejected(err){
        // err是一個TypeError異常
    }
)

3.3.7 構建可信任的Promise

Promise.resolve(..) 規範化傳入的值:

  • 傳入一個非Promise、非thenable的當即值, 會獲得一個用該值填充的Promise
  • 傳入一個真正的Promise,會返回同一個Promise
  • 傳入一個非Promise的thenable值,會試圖展開這個值,持續到提取出一個具體的非類Promise的最終值

具體看例子(傳入Promise的狀況略)

// 傳入一個當即值
let p = Promise.resolve(42);
p.then( res => {
    console.log('Promise.resolve(42).then:',res);
})
let p1 = Promise.resolve({});
p1.then( res => {
    console.log('Promise.resolve({}).then:',res);
})
// 傳入一個 thenable 嘗試展開
let p2 = Promise.resolve({
    then: function(cb) { cb(42)}
});
p2.then( res => {
    console.log('Promise.resolve(thenable).then:', res);
}, err => {
    console.log('Promise.resolve(thenable).then:', err);
})
// 注意 這種狀況其實也是當即值!!!
let p3 = Promise.resolve(
    setTimeout(()=>{
        return 'inside a continuation'  
    },1000)
); // settimeout函數返回當前定時器引用=>耶 當即值
p3.then( res => {
    console.log('Promise.resolve(看起來是個異步).then:', res); 
})

3.4 Promise鏈式流

Promise不只僅是一個單步執行this-then-that的操做機制,這只是它的構成部件,實際上Promise是能夠鏈接到一塊兒使用表示一系列異步步驟:

  • 每次對Promise調用then(..),它都會建立並返回一個新的Promise,咱們能夠將其連接起來;(並不侷限於要求then中返回一個Promise)
  • 無論從then(..)調用的完成回調(第一個參數)返回的值是什麼,它都會被自動設置爲被連接Promise(上一點中的)的完成(resolve)(必定要理解這句話)

再仔細看看第二點,結合上文 3.3.7 Promise.resolve(..)的能力,這是Promise鏈式流在每一步都能有異步能力的關鍵!

栗子:

// 返回當即值

    let p = Promise.resolve(21);
    p
    .then( function(v) {
        console.log(v);  // 21

        // 返回當即值
        return v * 2;
    })
    // 這裏是連接的Promise
    .then ( function(v) {
        console.log(v);  // 42
    });
// 返回Promise並引入異步

    let p = Promise.resolve(21);
    p
    .then ( function(v) {
        // 返回一個異步Promise
        return new Promise( (resolve, reject) => {
            setTimeout(() => {
                resolve(v*2);
            }, 1000);
        });
    })
    .then ( function(v) {
        // 前一步延遲1s後執行
        console.log(v);
    })

Promise鏈不只僅是一個表達多步異步序列的流程控制,還能夠從一個步驟到下一個步驟的消息通道

3.5 錯誤處理

幾種錯誤處理方式:

try...catch結構不能應用於異步模式

function foo() {
        setTimeout(() => {
            baz.bar();  // 錯誤代碼
        }, 100);
    }
    try{
        foo();  // 以後將拋出全局錯誤
    }
    catch (err) {
        // 不會走到這裏
    }

foo()中有本身的異步完成函數,其中任何異步錯誤都沒法捕捉到

node.js api或庫中常見的err-first模式

function foo(cb) {
        setTimeout(() => {
            try {
                var x = baz.bar();  //  錯誤代碼
                cb(null, x);
            }
            catch (err) {
                cb(err);
            }
        }, 100);
    }

    foo( function(err, val) {
        if(err) {
            console.error(err);  //  報錯惹
        }
        else {
            console.log(val);
        }
    })

分離回調模式(split-callback)
一個回調用於完成狀況,一個回調用於拒絕狀況
Promise採用的就是這種方式

先參考 3.3.6 再進行詳細討論:

Promise決議前、決議後產生的錯誤處理方式有所不一樣
錯誤的使用Promise API產生的錯誤會阻礙正常Promise對象的構造,這種狀況下會當即拋出異常(這種狀況應該死都不要出現 0 0)

3.5.1 絕望的陷阱

因爲Promise鏈式特色,其鏈上的最後一步,不論是什麼,老是存在着在未被查看的Promise中出現未捕獲錯誤的可能性

即理論上來講:總有可能有錯誤未被捕獲,而出現全局報錯

P.S. 這也是我的認爲使用Promise最頭疼的一點

3.5.2 處理未捕獲的狀況

關於如何解決3.5.1提出問題的一些思路

  • 增長done(..)做爲鏈式調用的終點,在其中能夠查看未捕獲的錯誤,而且不會建立和返回新的Promise
  • 依靠瀏覽器 追蹤Promise對象在被垃圾回收時是否有拒絕(未捕獲的錯誤),得到其報告 (什麼功能?@TODO),但是若是Promise未被垃圾回收呢?

3.5.2 成功的坑

該小節討論的是從做者角度提出一種避免在使用Promise時在開發者未注意的狀況下出現未捕獲錯誤而報出全局錯誤的方案

具體請看:

{
    let p = Promise.reject(21); // 將觸發全局報錯 Uncaught (in promise) 21

    let p1 = Promise.reject(21).then (  // 拒絕前,註冊了一個錯誤處理函數
        (res) => {
            // 不會走到這裏 
        },
        (err) => {
            console.log(`註冊了一個錯誤處理函數:${err}`);
        }
    )
    Promise.prototype.defer = function (){
        // 做者提出的一個API  
        // 簡單實現就是單純的返回這個Promise自己 
        return this;
    }

    let p2 = Promise.reject(21).defer(); // p2的結果在未來會被查看,如今暫時不要報全局錯誤

    let foo = Promise.resolve(21);

    foo
    .then (function(v) {
        return p2; // 這裏查看p2的結果
    }, function (err) {
        // 不會走到這裏
    })
    .catch (function(v) {
        console.log(v); // p2的結果
    })
}

3.6 Promise模式

基於Promise構建的異步抽象模式

3.6.1 Promise.all([ .. ])

相似門(gate)這種機制:須要等待兩個或更多並行/併發的任務都完成才能繼續,它們的完成順序並不重要,但必須都要完成,門才能打開並讓流程控制繼續

Promise.all([ .. ])的參數接收一個數組:

  • 數組中的每一個值都會交給Promise.resolve(..) 過濾以保證傳入值是一個真正的Promise (Promise.resolve(..)的做用參考 3.3.7 構建可信任的Promise
  • 數組爲空,主promise就會當即完成

返回一個Promise:

  • 傳入的全部promise完成,該promise標記完成,返回消息是一個由全部傳入promise的完成消息組成的數組,與調用API時傳入的順序一致(與完成順序無關)
  • 若是傳入的promise中有任何一個被拒絕的話,該promise會當即被拒絕,並丟棄來自其餘全部promise的所有結果(其餘promise仍是會執行),返回錯誤消息是被拒絕的那個promise的錯誤消息(注意,promise一旦決議結果不會變動,故僅有第一個被拒絕的promise錯誤消息會被主promise返回)

每一個promise都必須關聯一個拒絕/錯誤處理函數,特別是從Promise.all([ ... ])返回的那一個

3.6.2 Promise.race([ ... ])

相似門閂(shuan)競態:一旦有任何一個Promise決議爲完成,就標記爲完成;一旦有任何一個Promise決議爲拒絕,它就會拒絕

Promise.race([ ... ])的參數接收一個數組:

  • 被Promise.resolve(...)過濾那是固然的
  • 傳入當即值沒有任何意義,確定是第一個當即值取勝
  • 若是傳入一個空數組,會致使該Promise永遠不會決議!千萬不要這麼作

返回一個Promise:

  • 和Promise.all([ ... ])不一樣,返回消息不是一個數組,由於只能接收一個promise的完成消息

關於這兩個API須要注意

在all和race中存在着被忽略或丟棄的promise,若是這些promise中保存着重要的數據或資源或者開發者須要記錄這些promise失敗的事實,又該怎麼辦呢?

finally API就是基於這種狀況提出的:Promise須要一個finally(...)回調註冊,這個回調在Promise決議後老是會被調用,並容許執行任何須要的清理工做

注:書中提到finally還未被規範支持,而在18年1月已經正式加入到提案中了,可參考 https://github.com/tc39/propo...https://github.com/tc39/propo...

書中還提到了一種觀察模式(基於同一個Promise決議能夠被屢次查看),具體能夠看栗子

let foo = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(21);
        }, 301);
    });
    let timeout = function(time) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('timeout');
            }, time);
        })
    }
    // foo會被默默忽略
    Promise.race( [
        foo, 
        timeout(300)
    ])
    .then( (res) => {
        console.log(`Promise.race: ${res}`);
    })
    .finally( (res) => {
        console.log(`Promise.race: ${res}`);    // finally回調是不會提供任何參數的,詳情可看 https://github.com/tc39/proposal-promise-finally
    })
    // 觀察者模式
    if(!Promise.observe){
        Promise.observe = function(pr, cb){
            // 觀察pr的決議
            pr.then( 
                function fulfilled (msg){
                    // 完成時
                    Promise.resolve(msg).then(cb);
                },
                function reject (msg){
                    // 拒絕時 傳遞錯誤消息 但注意觀察者promise是resolve的
                    Promise.resolve(msg).then(cb);
                }
            );
            // 返回最初的promise
            return pr;
        }
    }
    // 仍是上一個超時的例子
    Promise.race( [
        Promise.observe(
            foo,
            function cleanup (msg){
                console.log(`Promise.observe: ${msg}`); // foo即便沒有在超時以前完成 也能夠獲取其決議狀況
            }
        )
        .then 
    ])

3.6.3 all([ .. ])和race([ .. ])的變體


@TODO 自行實現 Promise.any finally map等擴展API

3.6.4 併發迭代

實現一個異步的map(..)工具

  • 接收一個數組的值(能夠是Promise或其餘值)
  • 接收一個在每一個值上運行的一個函數
  • 返回一個Promise,其完成值是一個數組,該數組保存任務執行以後的異步完成值(保持映射順序)

這裏也主要看栗子

if(!Promise.map) {
        Promise.map = function(vals, cb) {
            // 等待全部map的promise決議的新的promise
            return Promise.all(
                // 對vals使用map將每一個值轉出promise,值數組->Promise數組
                vals.map( function(val){
                    // 將val值替換成調用cb函數後決議的新的promise
                    return new Promise( function(resolve){
                        // resolve reject傳入到cb函數中
                        cb(val, resolve);
                    })
                })
            )
        }
    }
    // 使用Promise.map
    var p1 = Promise.resolve(21);
    var p2 = Promise.resolve(30);
    var p3 = Promise.reject('opps');

    Promise.map( [p1,p2,p3], function(pr, resolve){
        Promise.resolve(pr)
        .then( val => {
            resolve( val*2 );
        },
            resolve  // 注意:不能發出拒絕信號,若是發出會致使Promise.map被拒絕,其餘map結果也會被丟棄
        )
    })
    .then( (vals) => {
        console.log(vals);
    })

TODO:Promise API 概述詳解單獨成篇

相關文章
相關標籤/搜索