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)系作者
  • 序言:七十年代末军熏,一起剝皮案震驚了整個(gè)濱河市轩猩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖均践,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晤锹,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡彤委,警方通過查閱死者的電腦和手機(jī)鞭铆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來焦影,“玉大人车遂,你說我怎么就攤上這事歉铝】藕” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵劳景,是天一觀的道長(zhǎng)椒涯。 經(jīng)常有香客問我柄沮,道長(zhǎng),這世上最難降的妖魔是什么废岂? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任祖搓,我火速辦了婚禮,結(jié)果婚禮上湖苞,老公的妹妹穿的比我還像新娘拯欧。我一直安慰自己,他們只是感情好财骨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布镐作。 她就那樣靜靜地躺著,像睡著了一般隆箩。 火紅的嫁衣襯著肌膚如雪该贾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天捌臊,我揣著相機(jī)與錄音杨蛋,去河邊找鬼。 笑死理澎,一個(gè)胖子當(dāng)著我的面吹牛逞力,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播糠爬,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼寇荧,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了执隧?” 一聲冷哼從身側(cè)響起揩抡,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤户侥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后捅膘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡滚粟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年寻仗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凡壤。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡署尤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出亚侠,到底是詐尸還是另有隱情曹体,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布硝烂,位于F島的核電站箕别,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏滞谢。R本人自食惡果不足惜串稀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望狮杨。 院中可真熱鬧母截,春花似錦、人聲如沸橄教。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽护蝶。三九已至华烟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間持灰,已是汗流浹背垦江。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留搅方,地道東北人比吭。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像姨涡,于是被迫代替她去往敵國和親衩藤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

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