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

Promise這一章的順序?qū)τ谖唇佑|過使用過Promise的童鞋而言略抽象了侠讯,前邊幾章主要為了說明Promise和之前的異步方式相比有什么優(yōu)勢和它能解決什么問題,后邊才詳解Promise的API設(shè)計和各種場景下如何使用Promise

建議先了解和簡單使用過Promise后再閱讀溉瓶,效果更佳

正文

3.1 什么是Promise

之前的方式:

  • 利用回調(diào)函數(shù)封裝程序中的continuation
  • 回調(diào)交給第三方
  • 第三方調(diào)用回調(diào)
  • 實現(xiàn)正確功能

Promise方式:
第三方提供了解其任務(wù)何時結(jié)束的能力

Promise的異步特性是基于任務(wù)的(圖示如下)


任務(wù)隊列.png

一種處理異步的思路:為了統(tǒng)一現(xiàn)在和將來,把它們都變成將來,即所有操作都成了異步的

書中關(guān)于Promise是個啥的觀點:

一種封裝和組合未來值的易于復用的機制
一種在異步任務(wù)中作為兩個或更多步驟的流程控制機制疟丙,時序上的this-then-that —— 關(guān)注點分離

Promise設(shè)計的重要基礎(chǔ)

  • Promise一定是異步執(zhí)行的余黎,即使是立即完成的Promise(類似 new Promise((resolve)=>{ resolve(42) }))重窟,也無法被同步觀察到
  • 一旦Promise決議,它就永遠保持在這個狀態(tài)惧财,變成了不變值(immuatable value)巡扇,這是設(shè)計中最基礎(chǔ)和最重要的因素
  • Promise至多只能有一個決議值(一個扭仁!一個!一個L琛)

引申:

  • Promise的決議結(jié)果可以給多方多次查看
  • 安全乖坠、可靠

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 調(diào)用過早

避免Zalgo這類副作用:一個任務(wù)有時同步完成刀闷,有時異步完成熊泵,可能導致競態(tài)條件

Promise從定義上保證了不會存在這種問題:參考3.1 設(shè)計基礎(chǔ) — 即使是立即完成的Promise,也無法被同步觀察到

3.3.2 調(diào)用過晚

Note: 調(diào)用過晚強調(diào)的是調(diào)用順序甸昏?

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

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

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

如果某個Promise一直不決議呢卒蘸?使用競態(tài)的高級抽象機制:

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

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

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

如果創(chuàng)建Promise的代碼試圖多次調(diào)用resolve(..)或reject(..),或者兩者都調(diào)用翻默,Promise只會接受第一次決議悬秉,后續(xù)調(diào)用都會被忽略

3.3.5 未能傳遞參數(shù)/環(huán)境值

Promise至多只能有一個決議值

如果使用多個參數(shù)調(diào)用resolve(..)或reject(..),第一個參數(shù)之后的所有參數(shù)都會被忽略

Promise其實也是傳入回調(diào)函數(shù)冰蘑,故函數(shù)中照樣能根據(jù)作用域規(guī)則訪問到對應的環(huán)境數(shù)據(jù)

3.3.6 吞掉錯誤或異常

這里說的錯誤或異澈兔冢可能出現(xiàn)在兩個過程:

  1. Promise創(chuàng)建過程或其決議確認之前的任何時間點上(注:書中原文查看其決議結(jié)果過程中任何時間點,個人認為可能翻譯得有點問題祠肥,應該要強調(diào)是其決議之前)
  2. Promise決議確認后在查看結(jié)果時(then(..)注冊的回調(diào)中)出現(xiàn)了js異常錯誤

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

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

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

針對2:
這個時候當前Promise已經(jīng)決議仇箱,其決議結(jié)果是個不可變值
then(..)調(diào)用返回的下一個Promise被拒絕

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

3.3.7 構(gòu)建可信任的Promise

Promise.resolve(..) 規(guī)范化傳入的值:

  • 傳入一個非Promise县恕、非thenable的立即值, 會得到一個用該值填充的Promise
  • 傳入一個真正的Promise剂桥,會返回同一個Promise
  • 傳入一個非Promise的thenable值忠烛,會試圖展開這個值,持續(xù)到提取出一個具體的非類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);
})
// 注意 這種情況其實也是立即值Hǘ骸C朗!
let p3 = Promise.resolve(
    setTimeout(()=>{
        return 'inside a continuation'  
    },1000)
); // settimeout函數(shù)返回當前定時器引用=>耶 立即值
p3.then( res => {
    console.log('Promise.resolve(看起來是個異步).then:', res); 
})

3.4 Promise鏈式流

Promise不僅僅是一個單步執(zhí)行this-then-that的操作機制斟薇,這只是它的構(gòu)成部件师坎,實際上Promise是可以連接到一起使用表示一系列異步步驟:

  • 每次對Promise調(diào)用then(..),它都會創(chuàng)建并返回一個新的Promise堪滨,我們可以將其鏈接起來胯陋;(并不局限于要求then中返回一個Promise)
  • 不管從then(..)調(diào)用的完成回調(diào)(第一個參數(shù))返回的值是什么,它都會被自動設(shè)置為被鏈接Promise(上一點中的)的完成(resolve)(一定要理解這句話)
    再仔細看看第二點,結(jié)合上文 3.3.7 Promise.resolve(..)的能力遏乔,這是Promise鏈式流在每一步都能有異步能力的關(guān)鍵义矛!

栗子:

// 返回立即值

    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后執(zhí)行
        console.log(v);
    })

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

3.5 錯誤處理

幾種錯誤處理方式:

try...catch結(jié)構(gòu)不能應用于異步模式


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

foo()中有自己的異步完成函數(shù)盟萨,其中任何異步錯誤都無法捕捉到

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);
        }
    })

分離回調(diào)模式(split-callback)
一個回調(diào)用于完成情況症革,一個回調(diào)用于拒絕情況
Promise采用的就是這種方式

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

Promise決議前、決議后產(chǎn)生的錯誤處理方式有所不同
錯誤的使用Promise API產(chǎn)生的錯誤會阻礙正常Promise對象的構(gòu)造鸯旁,這種情況下會立即拋出異常(這種情況應該死都不要出現(xiàn) 0 0)

3.5.1 絕望的陷阱

由于Promise鏈式特點,其鏈上的最后一步量蕊,不管是什么铺罢,總是存在著在未被查看的Promise中出現(xiàn)未捕獲錯誤的可能性

即理論上來說:總有可能有錯誤未被捕獲,而出現(xiàn)全局報錯

P.S. 這也是個人認為使用Promise最頭疼的一點

3.5.2 處理未捕獲的情況

關(guān)于如何解決3.5.1提出問題的一些思路

  • 增加done(..)作為鏈式調(diào)用的終點残炮,在其中可以查看未捕獲的錯誤韭赘,并且不會創(chuàng)建和返回新的Promise
  • 依靠瀏覽器 追蹤Promise對象在被垃圾回收時是否有拒絕(未捕獲的錯誤),獲得其報告 (什么功能势就?@TODO)泉瞻,可是如果Promise未被垃圾回收呢?

3.5.2 成功的坑

該小節(jié)討論的是從作者角度提出一種避免在使用Promise時在開發(fā)者未注意的情況下出現(xiàn)未捕獲錯誤而報出全局錯誤的方案

具體請看:

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

    let p1 = Promise.reject(21).then (  // 拒絕前苞冯,注冊了一個錯誤處理函數(shù)
        (res) => {
            // 不會走到這里 
        },
        (err) => {
            console.log(`注冊了一個錯誤處理函數(shù):${err}`);
        }
    )
    Promise.prototype.defer = function (){
        // 作者提出的一個API  
        // 簡單實現(xiàn)就是單純的返回這個Promise本身 
        return this;
    }

    let p2 = Promise.reject(21).defer(); // p2的結(jié)果在將來會被查看袖牙,現(xiàn)在暫時不要報全局錯誤

    let foo = Promise.resolve(21);

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

3.6 Promise模式

基于Promise構(gòu)建的異步抽象模式

3.6.1 Promise.all([ .. ])

類似門(gate)這種機制:需要等待兩個或更多并行/并發(fā)的任務(wù)都完成才能繼續(xù),它們的完成順序并不重要舅锄,但必須都要完成鞭达,門才能打開并讓流程控制繼續(xù)

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

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

返回一個Promise:

  • 傳入的所有promise完成皇忿,該promise標記完成畴蹭,返回消息是一個由所有傳入promise的完成消息組成的數(shù)組,與調(diào)用API時傳入的順序一致(與完成順序無關(guān))
  • 如果傳入的promise中有任何一個被拒絕的話鳍烁,該promise會立即被拒絕叨襟,并丟棄來自其他所有promise的全部結(jié)果(其他promise還是會執(zhí)行),返回錯誤消息是被拒絕的那個promise的錯誤消息(注意幔荒,promise一旦決議結(jié)果不會變更糊闽,故僅有第一個被拒絕的promise錯誤消息會被主promise返回)

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

3.6.2 Promise.race([ ... ])

類似門閂(shuan)爹梁,競態(tài):一旦有任何一個Promise決議為完成墓怀,就標記為完成;一旦有任何一個Promise決議為拒絕卫键,它就會拒絕

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

  • 被Promise.resolve(...)過濾那是當然的
  • 傳入立即值沒有任何意義傀履,肯定是第一個立即值取勝
  • 如果傳入一個空數(shù)組,會導致該Promise永遠不會決議!千萬不要這么做

返回一個Promise:

  • 和Promise.all([ ... ])不同钓账,返回消息不是一個數(shù)組碴犬,因為只能接收一個promise的完成消息

關(guān)于這兩個API需要注意

在all和race中存在著被忽略或丟棄的promise,如果這些promise中保存著重要的數(shù)據(jù)或資源或者開發(fā)者需要記錄這些promise失敗的事實梆暮,又該怎么辦呢服协?

finally API就是基于這種情況提出的:Promise需要一個finally(...)回調(diào)注冊,這個回調(diào)在Promise決議后總是會被調(diào)用啦粹,并允許執(zhí)行任何必要的清理工作

注:書中提到finally還未被規(guī)范支持偿荷,而在18年1月已經(jīng)正式加入到提案中了,可參考 https://github.com/tc39/proposals/blob/master/finished-proposals.mdhttps://github.com/tc39/proposal-promise-finally

書中還提到了一種觀察模式(基于同一個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回調(diào)是不會提供任何參數(shù)的跳纳,詳情可看 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 自行實現(xiàn) Promise.any finally map等擴展API

3.6.4 并發(fā)迭代

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

  • 接收一個數(shù)組的值(可以是Promise或其他值)
  • 接收一個在每個值上運行的一個函數(shù)
  • 返回一個Promise,其完成值是一個數(shù)組贪嫂,該數(shù)組保存任務(wù)執(zhí)行之后的異步完成值(保持映射順序)

這里也主要看栗子

   if(!Promise.map) {
        Promise.map = function(vals, cb) {
            // 等待所有map的promise決議的新的promise
            return Promise.all(
                // 對vals使用map將每個值轉(zhuǎn)出promise寺庄,值數(shù)組->Promise數(shù)組
                vals.map( function(val){
                    // 將val值替換成調(diào)用cb函數(shù)后決議的新的promise
                    return new Promise( function(resolve){
                        // resolve reject傳入到cb函數(shù)中
                        cb(val, resolve);
                    })
                })
            )
        }
    }
    // 栗子
    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  // 注意:不能發(fā)出拒絕信號,如果發(fā)出會導致Promise.map被拒絕力崇,其他map結(jié)果也會被丟棄
        )
    })
    .then( (vals) => {
        console.log(vals);
    })

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末斗塘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子亮靴,更是在濱河造成了極大的恐慌馍盟,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件茧吊,死亡現(xiàn)場離奇詭異朽合,居然都是意外死亡,警方通過查閱死者的電腦和手機饱狂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門曹步,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人休讳,你說我怎么就攤上這事讲婚。” “怎么了俊柔?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵筹麸,是天一觀的道長。 經(jīng)常有香客問我雏婶,道長物赶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任留晚,我火速辦了婚禮酵紫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己奖地,他們只是感情好橄唬,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著参歹,像睡著了一般仰楚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上犬庇,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天僧界,我揣著相機與錄音,去河邊找鬼臭挽。 笑死捂襟,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的埋哟。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼郎汪,長吁一口氣:“原來是場噩夢啊……” “哼赤赊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起煞赢,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤抛计,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后照筑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吹截,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年凝危,在試婚紗的時候發(fā)現(xiàn)自己被綠了波俄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡蛾默,死狀恐怖懦铺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情支鸡,我是刑警寧澤冬念,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站牧挣,受9級特大地震影響急前,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瀑构,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一裆针、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦据块、人聲如沸码邻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽像屋。三九已至,卻和暖如春边篮,著一層夾襖步出監(jiān)牢的瞬間己莺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工戈轿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留凌受,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓思杯,卻偏偏與公主長得像胜蛉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子色乾,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容