03 Promise2 信任問題

3.3 Promise 信任問題

回顧一下只用回調(diào)編碼的信任問題,把一個(gè)回調(diào)傳入工具foo()時(shí)可能出現(xiàn)如下問題:

  • 調(diào)用回調(diào)過早
  • 調(diào)用回調(diào)過晚(或不被調(diào)用)
  • 調(diào)用回調(diào)次數(shù)過少或過多
  • 未能傳遞所需的環(huán)境和參數(shù)
  • 吞掉可能出現(xiàn)的錯(cuò)誤和異常

Promise 的特性就是專門用來為這些問題提供一個(gè)有效的可復(fù)用的答案。

3.3.1 調(diào)用過早

根據(jù)定義,Promise就不必?fù)?dān)心這種問題,因?yàn)榧词故橇⒓赐瓿傻腜romise(類似于 new Promise(function(resolve){ resolve(42); }) )也無法被同步觀察到。

也就是說,對(duì)一個(gè)Promise調(diào)用then()的時(shí)候,即使這個(gè)Promise已經(jīng)決議,提供給then()的回調(diào)也總會(huì)被異步調(diào)用。

3.3.2 調(diào)用過晚

Promise創(chuàng)建對(duì)象調(diào)用resolve()或reject()時(shí),這個(gè)Promise的then()注冊(cè)的觀察回調(diào)就會(huì)被自動(dòng)調(diào)度??纱_信,這些被調(diào)度的回調(diào)在下一個(gè)異步事件點(diǎn)上一定會(huì)被觸發(fā)。
同步查看是不可能的,所以一個(gè)同步任務(wù)鏈無法以這種方式運(yùn)行來實(shí)現(xiàn)按照預(yù)期有效延遲另一個(gè)回調(diào)的發(fā)生。也就是說,一個(gè)Promise決議后,這個(gè)Promise上所有的通過then()注冊(cè)的回調(diào)都會(huì)在下一個(gè)異步時(shí)機(jī)點(diǎn)上依次被立即調(diào)用。這些回調(diào)中的任意一個(gè)都無法影響或延誤對(duì)其他回調(diào)的調(diào)用。

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

這里,“C” 無法打斷或搶占“B”,這是因?yàn)镻romise的運(yùn)作方式。

Promise 調(diào)度技巧
有很多調(diào)度的細(xì)微差別。這種情況下,兩個(gè)獨(dú)立Promise上鏈接的回調(diào)的相對(duì)順序無法可靠預(yù)測(cè)。
如果兩個(gè)Promise p1 和 p2都已經(jīng)決議,那么p1.then(), p2.then()應(yīng)該最終會(huì)制調(diào)用p1的回調(diào),然后是p2。但還有一些微妙的場(chǎng)景可能不是這樣。

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 , 而不是像你認(rèn)為的 B A

p1不是用立即值而是用另一個(gè)promise p3決議,后者本身決議為值“B”。規(guī)定的行為是把p3展開到p1,但是是異步地展開。所以,在異步任務(wù)隊(duì)列中,p1的回調(diào)排在p2的回調(diào)之后。
要避免這樣的細(xì)微區(qū)別帶來的噩夢(mèng),你永遠(yuǎn)都不應(yīng)該依賴于不同Promise間回調(diào)的順序和調(diào)度。實(shí)際上,好的編碼實(shí)踐方案根本不會(huì)讓多個(gè)回調(diào)的順序有絲毫影響,可能的話就要避免。

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

首先,沒有任何東西(甚至JS錯(cuò)誤)能阻止Prmise向你通知它的決議(如果它決議了的話)。如果你對(duì)一個(gè)Promise注冊(cè)了一個(gè)完成回調(diào)和一個(gè)拒絕回調(diào),那么Promise在決議時(shí)總是會(huì)調(diào)用其中一個(gè)。

當(dāng)然,如果你的回調(diào)函數(shù)本身包含JS錯(cuò)誤,那可能就會(huì)看不到你期望的結(jié)果。但實(shí)際上回調(diào)還是被調(diào)用了。后面討論,這些錯(cuò)誤并不會(huì)被吞掉。

但是,如果Promise永遠(yuǎn)不決議呢?即使這樣,Promise也提供了解決方案。其使用了一種稱為竟態(tài)的高級(jí)抽象機(jī)制:

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

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

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

根據(jù)定義,回調(diào)被調(diào)用的正確次數(shù)應(yīng)該是1。“過少”的情況就是調(diào)用0次,和前面解釋過的“未被”調(diào)用是同一種情況。
“過多”容易解釋。Promise的定義方式使得它只能被決議一次。如果出于某種原因,Promise創(chuàng)建代碼試圖調(diào)用resolve()或reject()多次,或者試圖兩者都調(diào)用,那么這個(gè)Promise將只會(huì)接受第一次決議,并默默地忽略任何后續(xù)調(diào)用。

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

當(dāng)然,如果你把同一個(gè)回調(diào)注冊(cè)了不止一次(比如p.then(f); p.then(f)),那頭被調(diào)用的次數(shù)就會(huì)和注冊(cè)次數(shù)相同。響應(yīng)函數(shù)只會(huì)被調(diào)用一次。

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

Promise 至多只能有一個(gè)決議值(完成或拒絕)。
如果你沒有用任何值顯式?jīng)Q議,那么這個(gè)值就是undefined,這是JS常見的處理方式。但不管這個(gè)值是什么,無論當(dāng)前或未來,它都會(huì)被傳給所有注冊(cè)的(且適當(dāng)?shù)耐瓿苫蚓芙^)回調(diào)。

還有一點(diǎn)需要清楚:如果使用多個(gè)參數(shù)調(diào)用resovel()或者reject()第一個(gè)參數(shù)之后的所有參數(shù)都會(huì)被默默忽略。

如果要傳遞多個(gè)值,你就必須要把它們封裝在一個(gè)數(shù)組或?qū)ο笾小?/p>

對(duì)環(huán)境來說,JS中的函數(shù)總是保持其定義所在的作用域的閉包,所以它們當(dāng)然可繼續(xù)你提供的環(huán)境狀態(tài)。

3.3.6 吞掉錯(cuò)誤或異常

如果在Promise的創(chuàng)建過程中或在查看其決議結(jié)果過程中的任何時(shí)間點(diǎn)上出現(xiàn)了一個(gè)JS異常錯(cuò)誤,比如一個(gè)TypeError或RefernceError,那這個(gè)異常就會(huì)被捕捉,并且會(huì)使這個(gè)Promise被拒絕。

var p = new Promise( function(resolve, reject){
    foo.bar();   // foo 未定義,所以會(huì)出錯(cuò)
    resolve(42);  // 永遠(yuǎn)不會(huì)到達(dá)這里
});
p.then(
    function fulfilled(){
        // 永遠(yuǎn)不會(huì)到這里
    },
    function rejected(err){
        // err 將會(huì)是一個(gè)TypeError異常對(duì)象來自foo.bar()這一行
    }
)

foo.bar()中發(fā)生的JS異常導(dǎo)致了Promise拒絕,你可捕捉并對(duì)其做出響應(yīng)。
Promise甚至把JS異常也變成了異步行為,進(jìn)而極大降低了竟態(tài)條件出現(xiàn)的可能。

但是,如果Promise完成后在查看結(jié)果時(shí)(then()注冊(cè)回調(diào)中)出現(xiàn)了JS異常錯(cuò)誤會(huì)怎樣呢?

var p = new Promise( function(resolve, reject){
    resolve( 42 );
});
p.then(
    function fulfilled(msg){
        foo.bar();
        console.log( msg );  // 永遠(yuǎn)不會(huì)到達(dá)這里
    },
    function rejected(err){
        // 永遠(yuǎn)也不會(huì)到達(dá)這里
    }
)

等一下,這看qvnn來像是foo.bar()產(chǎn)生的異常真的被吞掉了。別擔(dān)心,實(shí)際上并不是這樣。但是這里有一個(gè)深的問題。就是我們沒有偵聽到它。p.then()調(diào)用本身返回了另一個(gè)promise,正是這個(gè)promise將會(huì)因TypeError異常而被拒絕。

3.3.7 是可信任的 Promise 嗎

你肯定已經(jīng)注意到Promise并沒有完全擺脫回調(diào)。它們只是改變了傳遞回調(diào)的位置。我們并不是把回調(diào)傳遞給foo(),而是從foo()得到某個(gè)東西,然后把回調(diào)傳給這個(gè)東西。
但是,為什么這就比單純使用回調(diào)更值得信任呢?
關(guān)于Promise的很重要但是常常被忽略的一個(gè)細(xì)節(jié)是,Promise對(duì)這個(gè)問題已經(jīng)有一個(gè)解決方案。包含在原生ES6 Promise實(shí)現(xiàn)中的解決方案就是Promise.resolve()。

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

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

而如果向Promise.resolve() 傳遞一個(gè)真正的Promise,就只會(huì)返回同一個(gè)promise

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

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

var p = {
    then: function(cb){
        cb( 42 );
    }
};

// 這可以工作,但只是因?yàn)樾疫\(yùn)而已
p
.then(
    function fulfilled(val){
        console.log( val );  //42
    },
    function rejected(err){
        // 永遠(yuǎn)不會(huì)到這里
    }
)

但是,下面又會(huì)怎樣呢?

var p = {
    then: function(cb, errcb){
        cb(42);
        errcb("evil laugh");
    }
};

p
.then(
    function fulfilled(val){
      console.log( val );  //42
    },
    function rejected(err){
        // 啊,不應(yīng)該運(yùn)行!
        console.log( err );  // 邪惡的笑
    }
)

盡管如此,我們還是都可把這些版本的p 傳給Promise.resolve(),然后就會(huì)得到期望中的規(guī)范化后的安全結(jié)果:

Promise.resolve(p)
.then(
    function fulfilled(val){
        console.log(val);  //42
    },
    function rejected(err){
        // 永遠(yuǎn)不會(huì)到這里
    }
)

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

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

// 不要這么做
foo(42)
.then(function(v) {
      console.log( v );
});

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

對(duì)于用Promise.resolve() 為所有函數(shù)的返回值都封裝一層。另一個(gè)好處是,這樣做很容易把函數(shù)調(diào)用規(guī)范為定義良好的異步任務(wù)。如果foo(42)有時(shí)會(huì)返回一個(gè)立即值,有時(shí)會(huì)返回Promise,那么Promise.resolve(foo(42))就能保證總返回一個(gè)Promise結(jié)果。

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

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

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