Promise很多同學(xué)都聽說過邮旷,但是同學(xué)們真的了解Promise么?為什么要用它蝇摸,為什么要有它婶肩?以及它的出現(xiàn)為我們解決了怎么樣的問題,這些都是我們需要知道的貌夕,接下來我們一步步進(jìn)行分析律歼,由淺入深。
為什么要使用Promise 啡专?
在我們JavaScript大環(huán)境下险毁,我們的編程方式更多是基于異步編程,究竟什么是異步編程们童,為什么要異步編程畔况,我們之前的文章有過探討。在異步編程中使用的最多的就是回調(diào)函數(shù)慧库,先了解一下什么是回調(diào)函數(shù)跷跪。
回調(diào)函數(shù)指的是:被調(diào)用者回頭調(diào)用調(diào)用者的函數(shù),這種由調(diào)用方自己提供的函數(shù)叫回調(diào)函數(shù)齐板。
應(yīng)用場景舉例:
對于數(shù)組的filter方法吵瞻,里面實(shí)現(xiàn)的過濾的邏輯,實(shí)現(xiàn)了如何過濾甘磨,但是過濾的條件是什么橡羞,filter方法中提供回調(diào)函數(shù),讓我們自己寫济舆。為提高程序的通用性卿泽,調(diào)用者提供一個過濾條件函數(shù),這樣filter函數(shù)借此調(diào)用調(diào)用者的函數(shù)來進(jìn)行過濾吗冤。
應(yīng)用實(shí)例舉例:
[1,2,3,4,5].filter(item=> item % 2 ==0)
返回數(shù)組當(dāng)中偶數(shù)構(gòu)成的數(shù)組又厉。
在前端當(dāng)中涉及使用回調(diào)函數(shù)的地方非常的多。最常使用的地方在于我們發(fā)送Ajax請求椎瘟。一個請求發(fā)出去我們在等待結(jié)果覆致,就會有相應(yīng)的成功處理函數(shù),以及失敗處理函數(shù)肺蔚。這里處理函數(shù)指的就是我們的回調(diào)函數(shù)煌妈。同步回調(diào)沒有什么問題,真的回調(diào)問題在于異步,記下來我們一起來看璧诵。
讓我們異步回調(diào)的例子汰蜘,但讓我稍微修改它一下來畫出重點(diǎn):
// A和// B代表程序的前半部分(也就是現(xiàn)在),//C 標(biāo)識了程序的后半部分(也就是稍后)之宿。前半部分立即執(zhí)行族操,然后會出現(xiàn)一個不知多久的“暫停”比被。在未來某個時刻色难,如果Ajax 調(diào)用完成了,那么程序會回到它剛才離開的地方等缀,并繼續(xù)執(zhí)行后半部分枷莉。
換句話說,回調(diào)函數(shù)包裝或封裝了程序的延續(xù)尺迂。
讓我們把代碼弄得更簡單一些:
稍停片刻然后問你自己笤妙,你將如何描述(給一個不那么懂JS工作方式的人)這個程序的行為。
現(xiàn)在大多數(shù)同學(xué)可能在想或說著這樣的話:“做A噪裕,然后設(shè)置一個等待1000毫秒的定時器蹲盘,一旦它觸發(fā),就做C”州疾。與你的版本有多接近辜限?
你可能已經(jīng)發(fā)覺了不對勁兒的地方,給了自己一個修正版:“做A严蓖,設(shè)置一個1000毫秒的定時器薄嫡,然后做B,然后在超時事件觸發(fā)后颗胡,做C”毫深。這比第一個版本更準(zhǔn)確。你能發(fā)現(xiàn)不同之處嗎毒姨?
雖然第二個版本更準(zhǔn)確哑蔫,但是對于以一種將我們的大腦匹配代碼,代碼匹配JS引擎的方式講解這段代碼來說弧呐,這兩個版本都是不足的闸迷。這里的鴻溝既是微小的也是巨大的,而且是理解回調(diào)作為異步表達(dá)和管理的缺點(diǎn)的關(guān)鍵俘枫。
只要我們以回調(diào)函數(shù)的方式引入一個延遲時間(或者像許多程序員那樣引入幾十個P裙痢),我們就允許了一個分歧在我們的大腦如何工作和代碼將運(yùn)行的方式之間形成鸠蚪。當(dāng)這兩者背離時今阳,我們的代碼就不可避免地陷入這樣的境地:更難理解师溅,更難推理,更難調(diào)試盾舌,和更難維護(hù)墓臭。主要的原因在于我們的大腦。我們大腦的邏輯妖谴,也就是人正常的思維邏輯與這種回調(diào)的方式不符窿锉。人更擅長做完一件事,再做另一件事窖维。
接著來在一個實(shí)際場景下看看如何編程榆综,以及在書寫代碼過程中會出現(xiàn)怎么樣的問題。
問題:實(shí)現(xiàn)網(wǎng)上購票流程铸史。
解決上面的基本流程是:
1.查詢車票
2.查詢到車票,進(jìn)行購買
3.購買查詢車票怯伊,進(jìn)行占票
4.占票成功后進(jìn)行付款
5. 付款成功后打印車票
網(wǎng)上購票分為以上五個步驟琳轿,但是這里面每一步都是需要異步執(zhí)行的。
$.ajax({ // 發(fā)送查詢車票請求
這樣的代碼常被稱為“回調(diào)地獄(callbackhell)”耿芹,有時也被稱為“末日金字塔(pyramidof doom)”(由于嵌套的縮進(jìn)使它看起來像一個放倒的三角形)崭篡。
但是“回調(diào)地獄”實(shí)際上與嵌套/縮進(jìn)幾乎無關(guān)。我們可以把上面的代碼改寫成
樣的代碼組織形式幾乎看不出來有前一種形式的嵌套/縮進(jìn)困境吧秕,但它的每一處依然容易受到“回調(diào)地獄”的影響琉闪。為什么呢?
當(dāng)我們線性地(順序地)推理這段代碼砸彬,我們不得不從一個函數(shù)跳到下一個函數(shù)颠毙,再跳到下一個函數(shù),并在代碼中彈來彈去以“看到”順序流砂碉。
并且要記住蛀蜜,這個簡化的代碼風(fēng)格是某種最佳情況。我們都知道真實(shí)的JS程序代碼經(jīng)常更加神奇地錯綜復(fù)雜增蹭,使這樣量級的順序推理更加困難滴某。
另一件需要注意的事是:為了將第2,3滋迈,4步鏈接在一起使他們相繼發(fā)生霎奢,回調(diào)獨(dú)自給我們的啟示是將第2 步硬編碼在第1 步中,將第3 步硬編碼在第2 步中饼灿,將第4 步硬編碼在第3 步中幕侠,如此繼續(xù)。硬編碼不一定是一件壞事赔退,如果第2步應(yīng)當(dāng)總是在第3步之前真的是一個固定條件橙依。
不過硬編碼絕對會使代碼變得更脆弱证舟,因?yàn)樗豢紤]任何可能使在步驟前行的過程中出現(xiàn)偏差的異常情況。舉個例子窗骑,如果第2步失敗了女责,第3步永遠(yuǎn)不會到達(dá),第2步也不會重試创译,或者移動到一個錯誤處理流程上抵知,等等。
所有這些問題你都可以手動硬編碼在每一步中软族,但那樣的代碼總是重復(fù)性的刷喜,而且不能在其他步驟或你程序的其他異步流程中復(fù)用。
即便我們的大腦可能以順序的方式規(guī)劃一系列任務(wù)(這個立砸,然后這個掖疮,然后這個),但我們大腦運(yùn)行的事件的性質(zhì)颗祝,使恢復(fù)/重試/分流這樣的流程控制幾乎毫不費(fèi)力浊闪。如果你出去購物,而且你發(fā)現(xiàn)你把購物單忘在家里了螺戳,這并不會因?yàn)槟銢]有提前計劃這種情況而結(jié)束這一天搁宾。你的大腦會很容易地繞過這個小問題:你回家,取購物單倔幼,然后回頭去商店盖腿。
但是手動硬編碼的回調(diào)(甚至帶有硬編碼的錯誤處理)的脆弱本性通常不那么優(yōu)雅。一旦你最終指明了(也就是提前規(guī)劃好了)所有各種可能性/路徑损同,代碼就會變得如此復(fù)雜以至于幾乎不能維護(hù)或更新翩腐。
這才是“回調(diào)地獄”想表達(dá)的!嵌套/縮進(jìn)基本上一個余興表演揖庄,盡管看起來還是有些不方便的栗菜。
上面是多個回調(diào)配合著嵌套產(chǎn)生的回調(diào)地獄問題,回調(diào)還會產(chǎn)生信任問題蹄梢。在順序的大腦規(guī)劃和JS代碼中回調(diào)驅(qū)動的異步處理間的不匹配只是關(guān)于回調(diào)的問題的一部分疙筹。還有一些更深刻的問題值得擔(dān)憂。
讓我們再一次重溫這個概念——回調(diào)函數(shù)是我們程序的延續(xù)(也就是程序的第二部分):
// A和// B 現(xiàn)在發(fā)生禁炒,在 JS主程序的直接控制之下而咆。但是//C 被推遲到稍后再發(fā)生,并且在另一部分的控制之下——這里是ajax(..)函數(shù)幕袱。在基本的感覺上暴备,這樣的控制交接一般不會讓程序產(chǎn)生很多問題。
但是不要被這種控制切換不是什么大事的罕見情況欺騙了们豌。事實(shí)上涯捻,它是回調(diào)驅(qū)動的設(shè)計的最可怕的(也是最微妙的)問題浅妆。這個問題圍繞著一個想法展開:有時ajax(..)(或者說你向之提交回調(diào)的部分)不是你寫的函數(shù),或者不是你可以直接控制的函數(shù)障癌。很多時候它是一個由第三方提供的工具凌外。當(dāng)你把你程序的一部分拿出來并把它執(zhí)行的控制權(quán)移交給另一個第三方時,我們稱這種情況為“控制反轉(zhuǎn)”涛浙。在你的代碼和第三方工具之間有一個沒有明言的“契約”——一組你期望被維護(hù)的東西康辑。
你是一個開發(fā)者,正在建造一個販賣昂貴電視的網(wǎng)站的結(jié)算系統(tǒng)轿亮。你已經(jīng)將結(jié)算系統(tǒng)的各種頁面順利地制造完成疮薇。在最后一個頁面,當(dāng)用戶點(diǎn)解“確定”購買電視時我注,你需要調(diào)用一個第三方函數(shù)(假如由一個跟蹤分析公司提供)按咒,以便使這筆交易能夠被追蹤。
你注意到它們提供的是某種異步追蹤工具仓手,也許是為了最佳的性能胖齐,這意味著你需要傳遞一個回調(diào)函數(shù)。在你傳入的這個程序的延續(xù)中嗽冒,有你最后的代碼——劃客人的信用卡并顯示一個感謝頁面。
這段代碼可能看起來像這樣:
足夠簡單补履,對吧添坊?你寫好代碼,測試它箫锤,一切正常贬蛙,然后你把它部署到生產(chǎn)環(huán)境。大家都很開心谚攒!
若干個月過去了阳准,沒有任何問題。你幾乎已經(jīng)忘了你曾寫過的代碼馏臭。一天早上野蝇,工作之前你先在咖啡店坐坐,悠閑地享用著你的拿鐵括儒,直到你接到老板慌張的電話要求你立即扔掉咖啡并沖進(jìn)辦公室绕沈。當(dāng)你到達(dá)時,你發(fā)現(xiàn)一位高端客戶為了買同一臺電視信用卡被劃了5次帮寻,而且可以理解乍狐,他不高興」潭海客服已經(jīng)道了歉并開始辦理退款浅蚪。但你的老板要求知道這是怎么發(fā)生的藕帜。“我們沒有測試過這樣的情況嗎OО痢洽故?”
你甚至不記得你寫過的代碼了。但你還是往回挖掘試著找出是什么出錯了操漠。在分析過一些日志之后收津,你得出的結(jié)論是,唯一的解釋是分析工具不知怎么的浊伙,也就是第三方的函數(shù)出了問題撞秋,由于某些原因,將你的回調(diào)函數(shù)調(diào)用了5 次而非一次嚣鄙。他們的文檔中沒有任何東西提到此事吻贿。
十分令人沮喪,你聯(lián)系了客戶支持哑子,當(dāng)然他們和你一樣驚訝舅列。他們同意將此事向上提交至開發(fā)者,并許諾給你回復(fù)卧蜓。第二天帐要,你收到一封很長的郵件解釋他們發(fā)現(xiàn)了什么,然后你將它轉(zhuǎn)發(fā)給了你的老板弥奸。
看起來榨惠,分析公司的開發(fā)者曾經(jīng)制作了一些實(shí)驗(yàn)性的代碼,在一定條件下盛霎,將會每秒重試一次收到的回調(diào)赠橙,在超時之前共計5秒。他們從沒想要把這部分推到生產(chǎn)環(huán)境愤炸,但不知怎地他們這樣做了期揪,而且他們感到十分難堪而且抱歉。然后是許多他們?nèi)绾味ㄎ诲e誤的細(xì)節(jié)规个,和他們將要如何做以保證此事不再發(fā)生凤薛。等等,等等绰姻。你找你的老板談了此事枉侧,但是他對事情的狀態(tài)不是感覺特別舒服。他堅持狂芋,而且你也勉強(qiáng)地同意榨馁,你不能再相信他們了,而你將需要指出如何保護(hù)放出的代碼帜矾,使它們不再受這樣的漏洞威脅翼虫。
修修補(bǔ)補(bǔ)之后屑柔,你實(shí)現(xiàn)了一些如下的特殊邏輯代碼,團(tuán)隊(duì)中的每個人看起來都挺喜歡:
在這里我們實(shí)質(zhì)上創(chuàng)建了一個門閥來處理我們的回調(diào)被并發(fā)調(diào)用多次的情況珍剑。
但一個QA 的工程師問掸宛,“如果他們沒調(diào)你的回調(diào)怎么辦?”噢招拙。誰也沒想過唧瘾。
你開始布下天羅地網(wǎng),考慮在他們調(diào)用你的回調(diào)時所有出錯的可能性别凤。這里是你得到的分析工具可能不正常運(yùn)行的方式的大致列表:
調(diào)用回調(diào)過早(在它開始追蹤之前)
調(diào)用回調(diào)過晚(或不調(diào))
調(diào)用回調(diào)太少或太多次(就像你遇到的問題J涡颉)
沒能向你的回調(diào)傳遞必要的環(huán)境/參數(shù)
吞掉了可能發(fā)生的錯誤/異常
...
這感覺像是一個麻煩清單,因?yàn)樗褪枪婺摹D憧赡苈_始理解求豫,你將要不得不為每一個傳遞到你不能信任的工具中的回調(diào)都創(chuàng)造一大堆的特殊邏輯。
在上面的過程中我們發(fā)現(xiàn)幾個大問題:
1. 回調(diào)配合著嵌套會產(chǎn)生回調(diào)地獄問題诉稍,思路很不清晰蝠嘉。
2. 由于回調(diào)存在著依賴反轉(zhuǎn),在使用第三方提供的方法時杯巨,存在信任問題蚤告。
3. 當(dāng)我們不寫錯誤的回調(diào)函數(shù)時,會存在異常無法捕獲
4.導(dǎo)致我們的性能更差服爷,本來可以一起做的但是使用回調(diào)罩缴,導(dǎo)致多件事導(dǎo)致我們的性能更差,本來可以一起做的但是使用回調(diào)层扶,導(dǎo)致多件事情順序執(zhí)行,用的時間更多
針對這樣的問題我們該怎么解決呢烙荷?
我們首先想要解決的是信任問題镜会,信任是如此脆弱而且是如此的容易丟失≈粘椋回想一下戳表,我們將我們的程序的延續(xù)包裝進(jìn)一個回調(diào)函數(shù)中,將這個回調(diào)交給另一個團(tuán)體(甚至是潛在的外部代碼)昼伴,并雙手合十祈禱它會做正確的事情并調(diào)用這個回調(diào)匾旭。
我們這么做是因?yàn)槲覀兿胝f,“這是稍后將要發(fā)生的事圃郊,在當(dāng)前的步驟完成之后价涝。”但是如果我們能夠反向倒轉(zhuǎn)這種控制反轉(zhuǎn)呢持舆?如果不是將我們程序的延續(xù)交給另一個團(tuán)體色瘩,而是希望它返回給我們一個可以知道它何時完成的能力伪窖,然后我們的代碼可以決定下一步做什么呢?
這種規(guī)范被稱為Promise居兆。
Promise正在像風(fēng)暴一樣席卷JS世界覆山,因?yàn)殚_發(fā)者和語言規(guī)范作者之流拼命地想要在他們的代碼/設(shè)計中結(jié)束回調(diào)地獄的瘋狂。事實(shí)上泥栖,大多數(shù)新被加入JS/DOM 平臺的異步API 都是建立在Promise 之上的簇宽。
什么是Promise ?
Promise是異步編程的一種解決方案:從語法上講吧享,promise是一個對象魏割,從它可以獲取異步操作的消息;從本意上講耙蔑,它是承諾见妒,承諾它過一段時間會給你一個結(jié)果盟萨。promise有三種狀態(tài):pending(等待態(tài))熬芜,fulfiled(成功態(tài))接谨,rejected(失敗態(tài))总放;狀態(tài)一旦改變侄非,就不會再變窥妇。創(chuàng)造promise實(shí)例后屿脐,它會立即執(zhí)行疯淫。一般來說我們會碰到的回調(diào)嵌套都不會很多牲尺,一般就一到兩級卵酪,但是某些情況下,回調(diào)嵌套很多時谤碳,代碼就會非常繁瑣溃卡,會給我們的編程帶來很多的麻煩,這種情況俗稱——回調(diào)地獄蜒简。
這時候我們的promise 就應(yīng)運(yùn)而生瘸羡、粉墨登場了。
Promise的基本使用
Promise是一個構(gòu)造函數(shù)搓茬,自己身上有all犹赖、reject、resolve這幾個眼熟的方法卷仑,原型上有then峻村、catch等同樣很眼熟的方法。
那就new 一個
Promise的構(gòu)造函數(shù)接收一個參數(shù):函數(shù)锡凝,并且這個函數(shù)需要傳入兩個參數(shù):
Resolve:異步操作執(zhí)行成功后的回調(diào)函數(shù)
reject:異步操作執(zhí)行失敗后的回調(diào)函數(shù)
then鏈?zhǔn)讲僮鞯挠梅?/p>
所以粘昨,從表面上看,Promise只是能夠簡化層層回調(diào)的寫法,而實(shí)質(zhì)上Promise的精髓是“狀態(tài)”雾棺,用維護(hù)狀態(tài)膊夹、傳遞狀態(tài)的方式來使得回調(diào)函數(shù)能夠及時調(diào)用,它比傳遞callback 函數(shù)要簡單捌浩、靈活的多放刨。所以使用Promise的正確場景是這樣的,把我們之前的問題修改一下:
通過Promise這種方式很好的解決了回調(diào)地獄問題尸饺,使得異步過程同步化进统,讓代碼的整體邏輯與大腦的思維邏輯一致,減少出錯率浪听。
reject的用法:
把Promise 的狀態(tài)置為rejected螟碎,這樣我們在then中就能捕捉到,然后執(zhí)行“失敗”情況的回調(diào)迹栓,看下面的代碼掉分。
then中傳了兩個參數(shù),then方法可以接受兩個參數(shù)克伊,第一個對應(yīng)resolve的回調(diào)酥郭,第二個對應(yīng)reject的回調(diào)。所以我們能夠分別拿到他們傳過來的數(shù)據(jù)愿吹。多次運(yùn)行這段代碼不从,你會隨機(jī)得到兩種結(jié)果
catch的用法
我們知道Promise 對象除了then 方法,還有一個catch 方法犁跪,它是做什么用的呢椿息?其實(shí)它和then 的第二個參數(shù)一樣,用來指定reject 的回調(diào)坷衍。用法是這樣:
效果和寫在then 的第二個參數(shù)里面一樣寝优。不過它還有另外一個作用:在執(zhí)行resolve 的回調(diào)(也就是上面then 中的第一個參數(shù))時,如果拋出異常了(代碼出錯了)枫耳,那么并不會報錯卡死js倡勇,而是會進(jìn)到這個catch方法中。請看下面的代碼:
在resolve 的回調(diào)中嘉涌,我們console.log(somedata);而somedata這個變量是沒有被定義的。如果我們不用Promise夸浅,代碼運(yùn)行到這里就直接在控制臺報錯了仑最,不往下運(yùn)行了,也就是說進(jìn)到catch方法里面去了帆喇,而且把錯誤原因傳到了reason參數(shù)中警医。即便是有錯誤的代碼也不會報錯了,這與我們的try/catch 語句有相同的功能。
all的用法:
誰跑的慢预皇,以誰為準(zhǔn)執(zhí)行回調(diào)侈玄。all接收一個數(shù)組參數(shù),里面的值最終都算返回Promise 對象吟温。
Promise的all 方法提供了并行執(zhí)行異步操作的能力序仙,并且在所有異步操作執(zhí)行完后才執(zhí)行回調(diào)÷澈溃看下面的例子:
有了all潘悼,你就可以并行執(zhí)行多個異步操作,并且在一個回調(diào)中處理所有的返回數(shù)據(jù)爬橡,是不是很酷治唤?有一個場景是很適合用這個的,一些游戲類的素材比較多的應(yīng)用糙申,打開網(wǎng)頁時宾添,預(yù)先加載需要用到的各種資源如圖片、flash以及各種靜態(tài)文件柜裸。所有的都加載完后缕陕,我們再進(jìn)行頁面的初始化。在這里可以解決時間性能的問題粘室,我們不需要在把每個異步過程同步出來榄檬。
race的用法:
誰跑的快,以誰為準(zhǔn)執(zhí)行回調(diào)
race的使用場景:比如我們可以用race 給某個異步請求設(shè)置超時時間衔统,并且在超時后執(zhí)行相應(yīng)的操作鹿榜,代碼如下:
接下來再說一說Promise 解決回調(diào)信任問題。
回顧一下只用回調(diào)編碼的信任問題锦爵,把一個回調(diào)傳入工具foo()時可能出現(xiàn)如下問題:
調(diào)用回調(diào)過早
調(diào)用回調(diào)過晚(或不被調(diào)用)
調(diào)用回調(diào)次數(shù)過少或過多
未能傳遞所需的環(huán)境和參數(shù)
吞掉可能出現(xiàn)的錯誤和異常
Promise的特性就是專門用來為這些問題提供一個有效的可復(fù)用的答案舱殿。
調(diào)用過早
根據(jù)定義,Promise就不必?fù)?dān)心這種問題险掀,因?yàn)榧词故橇⒓赐瓿傻腜romise(類似于new Promise(function(resolve){ resolve(42); }))也無法被同步觀察到沪袭。
也就是說,對一個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)在下一個異步事件點(diǎn)上一定會被觸發(fā)毅该。
同步查看是不可能的博秫,所以一個同步任務(wù)鏈無法以這種方式運(yùn)行來實(shí)現(xiàn)按照預(yù)期有效延遲另一個回調(diào)的發(fā)生。也就是說眶掌,一個Promise 決議后挡育,這個Promise 上所有的通過then()注冊的回調(diào)都會在下一個異步時機(jī)點(diǎn)上依次被立即調(diào)用。這些回調(diào)中的任意一個都無法影響或延誤對其他回調(diào)的調(diào)用朴爬。
這里即寒,“C”無法打斷或搶占“B”,這是因?yàn)镻romise 的運(yùn)作方式寝殴。
Promise調(diào)度技巧
有很多調(diào)度的細(xì)微差別蒿叠。這種情況下,兩個獨(dú)立Promise 上鏈接的回調(diào)的相對順序無法可靠預(yù)測蚣常。
如果兩個Promise p1 和p2 都已經(jīng)決議市咽,那么p1.then(),p2.then()應(yīng)該最終會制調(diào)用p1 的回調(diào)抵蚊,然后是p2施绎。但還有一些微妙的場景可能不是這樣。
// AB , 而不是像你認(rèn)為的B A
p1不是用立即值而是用另一個promise p3 決議贞绳,后者本身決議為值“B”谷醉。
規(guī)定的行為是把p3 展開到p1,但是是異步地展開。所以冈闭,在異步任務(wù)隊(duì)列中俱尼,p1的回調(diào)排在p2 的回調(diào)之后。
要避免這樣的細(xì)微區(qū)別帶來的噩夢萎攒,你永遠(yuǎn)都不應(yīng)該依賴于不同Promise間回調(diào)的順序和調(diào)度遇八。實(shí)際上,好的編碼實(shí)踐方案根本不會讓多個回調(diào)的順序有絲毫影響耍休,可能的話就要避免刃永。
回調(diào)未調(diào)用
首先,沒有任何東西(甚至JS 錯誤)能阻止Prmise 向你通知它的決議(如果它決議了的話)羊精。如果你對一個Promise 注冊了一個完成回調(diào)和一個拒絕回調(diào)斯够,那么Promise 在決議時總是會調(diào)用其中一個。
當(dāng)然喧锦,如果你的回調(diào)函數(shù)本身包含JS錯誤读规,那可能就會看不到你期望的結(jié)果。但實(shí)際上回調(diào)還是被調(diào)用了燃少。后面討論掖桦,這些錯誤并不會被吞掉。
但是供汛,如果Promise 永遠(yuǎn)不決議呢?即使這樣,Promise也提供了解決方案怔昨。其使用了一種稱為竟態(tài)的高級抽象機(jī)制:
我們可保證一個foo()有一個信號雀久,防止其永久掛住程序。
調(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)用仁期,那么這個Promise 將只會接受第一次決議桑驱,并默默地忽略任何后續(xù)調(diào)用。
由于Promise 只能被決議一次跛蛋,所以任何通過then()注冊的(每個)回調(diào)就只會被調(diào)用一次熬的。
當(dāng)然,如果你把同一個回調(diào)注冊了不止一次(比如p.then(f);p.then(f))赊级,那頭被調(diào)用的次數(shù)就會和注冊次數(shù)相同押框。響應(yīng)函數(shù)只會被調(diào)用一次。
未能傳遞參數(shù)/環(huán)境值
Promise至多只能有一個決議值(完成或拒絕)理逊。
如果你沒有用任何值顯式?jīng)Q議橡伞,那么這個值就是undefined,這是JS常見的處理方式挡鞍。但不管這個值是什么骑歹,無論當(dāng)前或未來,它都會被傳給所有注冊的(且適當(dāng)?shù)耐瓿苫蚓芙^)回調(diào)墨微。
還有一點(diǎn)需要清楚:如果使用多個參數(shù)調(diào)用resovel()或者reject()第一個參數(shù)之后的所有參數(shù)都會被默默忽略道媚。
如果要傳遞多個值,你就必須要把它們封裝在一個數(shù)組或?qū)ο笾小?/p>
對環(huán)境來說翘县,JS中的函數(shù)總是保持其定義所在的作用域的閉包最域,所以它們當(dāng)然可繼續(xù)你提供的環(huán)境狀態(tài)。
吞掉錯誤或異常
如果在Promise 的創(chuàng)建過程中或在查看其決議結(jié)果過程中的任何時間點(diǎn)上出現(xiàn)了一個JS 異常錯誤锈麸,比如一個TypeError 或RefernceError镀脂,那這個異常就會被捕捉,并且會使這個Promise 被拒絕忘伞。
foo.bar()中發(fā)生的JS異常導(dǎo)致了Promise拒絕薄翅,你可捕捉并對其做出響應(yīng)沙兰。
Promise甚至把JS 異常也變成了異步行為,進(jìn)而極大降低了竟態(tài)條件出現(xiàn)的可能翘魄。
但是鼎天,如果Promise 完成后在查看結(jié)果時(then()注冊回調(diào)中)出現(xiàn)了JS異常錯誤會怎樣呢?
等一下暑竟,這看qvnn?來像是foo.bar()產(chǎn)生的異常真的被吞掉了斋射。別擔(dān)心,實(shí)際上并不是這樣但荤。但是這里有一個深的問題罗岖。就是我們沒有偵聽到它。
p.then()調(diào)用本身返回了另一個promise腹躁,正是這個promise 將會因TypeError異常而被拒絕桑包。
是可信任的Promise 嗎
你肯定已經(jīng)注意到Promise 并沒有完全擺脫回調(diào)。它們只是改變了傳遞回調(diào)的位置潜慎。我們并不是把回調(diào)傳遞給foo()捡多,而是從foo()得到某個東西,然后把回調(diào)傳給這個東西铐炫。
但是垒手,為什么這就比單純使用回調(diào)更值得信任呢?
關(guān)于Promise 的很重要但是常常被忽略的一個細(xì)節(jié)是倒信,Promise對這個問題已經(jīng)有一個解決方案科贬。包含在原生ES6 Promise 實(shí)現(xiàn)中的解決方案就是Promise.resolve()。
如果向Promise.resolve()傳遞一個非Promise鳖悠、非thenable 的立即值榜掌,就會得到一個用這個值填充的promise。下面這種情況下乘综,promisep1 和promise p2的行為是完全一樣的:
如果向Promise.resolve()傳遞了一個非Promise 的thenable 值憎账,前者會試圖展開這個值,而且展開過程會持續(xù)到提取出一個具體的非類Promise 的最終值卡辰。
盡管如此胞皱,我們還是都可把這些版本的p 傳給Promise.resolve(),然后就會得到期望中的規(guī)范化后的安全結(jié)果:
Promise.resolve()可接受任何thenable九妈,將其解封完它的非thenable 值反砌。從Promise.resolve()得到的是一個真正的Promise,是一個可信任的值。如果你傳入的已經(jīng)是真正的Promise萌朱,那們你得到的就是它本身宴树,所以通過Promise.resolve()過濾來獲得可信任性完全沒有壞處。
假設(shè)我們要調(diào)用一個工具foo()晶疼,且不確定得到的返回值是否是一個可信任的行為良好的Promise酒贬,但我們可知道它至少是一個thenable又憨。
Promise.resolve()提供了可信任的Promise 封裝工具,可鏈接使用:
對于用Promise.resolve()為所有函數(shù)的返回值都封裝一層锭吨。另一個好處是竟块,這樣做很容易把函數(shù)調(diào)用規(guī)范為定義良好的異步任務(wù)。如果foo(42)有時會返回一個立即值耐齐,有時會返回Promise,那么Promise.resolve(foo(42))就能保證總返回一個Promise 結(jié)果蒋情。
原創(chuàng)聲明:本文內(nèi)容為渡一教育原創(chuàng)作品埠况,未經(jīng)授權(quán),不可轉(zhuǎn)載棵癣。