你不知道的js(中卷)第8章 Promise

上一章講到,用回調(diào)來實(shí)現(xiàn)異步的兩大問題:代碼缺乏順序性;控制權(quán)交出,缺乏可信任性。

先說可信任性:傳遞回調(diào)的代碼,是把控制權(quán)交給第三方,因而難以信任。
假如讓第三方告訴我們其任務(wù)何時(shí)結(jié)束,然后由我們自己決定下一步操作呢?
這種范式就叫做Promise。
實(shí)際上,絕大多數(shù)JavaScript/DOM平臺(tái)新增的異步API都是基于Promise的。

1.什么是Promise

在展示 Promise 代碼之前,先從概念上完整地解釋 Promise 到底是什么。

1.1 未來值

可以不關(guān)心一個(gè)值現(xiàn)在還是將來會(huì)得到,用一個(gè)占位符來表示它,這個(gè)占位符使得這個(gè)值不再依賴時(shí)間,我們可以在還沒拿到值(未來才能拿到,當(dāng)然也可能拿失?。┑臅r(shí)候就編寫整個(gè)事情的代碼。

①現(xiàn)在值和將來值
??????不管一個(gè)值是現(xiàn)在就能拿到,還是將來才能拿到,我們可以用同樣的方式去對(duì)待它,不管它是不是異步,都可以不關(guān)心,都不影響代碼邏輯和寫法。
②Promise值
??????作者舉了個(gè)Promise的使用例子(用Promise來寫“計(jì)算未來值x和未來值y的和”),并講解了一下例子中語句的含義和特性。
??????從外部看,由于Promise封裝了依賴于時(shí)間的狀態(tài)——等待底層值的完成或拒絕,所以Promise本身是與時(shí)間無關(guān)的。因此,Promise可以按照可預(yù)測(cè)的方式組成(組合),而不用關(guān)心時(shí)序或底層的結(jié)果。
??????另外,一旦 Promise 決議,它就永遠(yuǎn)保持在這個(gè)狀態(tài),可以根據(jù)需求多次查看。
??????Promise決議后就是外部不可變的值(immutable value),我們可以安全地把這個(gè)值傳遞給第三方,并確信它不會(huì)被有意無意地修改。
??????Promise是一種封裝和組合未來值的易于復(fù)用的機(jī)制。

1.2 完成事件

??????Promise除了可以看作是表示“未來值”的東西,也可以用來表示一種在異步任務(wù)中的多個(gè)步驟的流程控制機(jī)制,它控制多個(gè)異步步驟的時(shí)序。
??????假如有個(gè)異步步驟foo,我們不關(guān)心其細(xì)節(jié),只想在該步驟結(jié)束以后得到通知,并做一些處理。
??????典型的寫法是給foo傳入回調(diào)方法,通知就是foo調(diào)用的回調(diào)。
??????而使用Promise的話,Promise偵聽foo的事件(成功事件和失敗事件),偵聽到事件以后,就算得到了通知,然后再根據(jù)通知做一些處理。
??????回調(diào)是對(duì)控制關(guān)系的反轉(zhuǎn),Promise這種范式則是控制關(guān)系的反轉(zhuǎn)的反轉(zhuǎn)。
??????這樣做的好處是,foo不用關(guān)心要調(diào)哪些回調(diào),只用把結(jié)果告訴Promise,關(guān)心任務(wù)結(jié)果的人自然會(huì)去監(jiān)聽Promise的。這樣就實(shí)現(xiàn)了關(guān)注點(diǎn)的分離。
??????從本質(zhì)上說,Promise對(duì)象就是分離的關(guān)注點(diǎn)之間一個(gè)中立的第三方協(xié)商機(jī)制。



2.具有then方法的鴨子類型

??????如何確定一個(gè)值是Promise?
??????instanceof Promise是不夠用的,因?yàn)镻romise值可能是從其它iframe拿到的,也有可能是其它庫或框架自己實(shí)現(xiàn)的Promise,而不是用原生ES6實(shí)現(xiàn)的(在不支持ES6的瀏覽器中就可能會(huì)有)。這些情況都沒法用instanceof Promise來判斷。

??????我們根據(jù)一個(gè)值具有哪些屬性來對(duì)這個(gè)值的類型做出一些假定。這種類型檢查一般稱為鴨子類型(duck typing)——“如果它看起來像只鴨子,叫起來像只鴨子,那它一定就是只鴨子。”
??????所以對(duì)一個(gè)值是否是Promise的檢測(cè),就會(huì)是類似這樣:判讀它 是一個(gè)對(duì)象 且 有then屬性 且 then屬性是一個(gè)function
??????但這樣的判斷是有風(fēng)險(xiǎn)的,比如很多類庫提供了具有then方法的對(duì)象/委托,就會(huì)被鴨子類型判斷為Promise。這些類庫要么不得不把自己的方法重新命名了以避免沖突,要么被打?yàn)椤芭c含有Promise的代碼不兼容”。
??????如果有其它代碼無意或惡意地給Object.prototype、Array.prototype或者其它原生原型添加了then方法,也會(huì)造成災(zāi)難。
??????不過鴨子類型有時(shí)候還是有用的,只是要小心鴨子類型把不是Promise的值誤判為Promise的情況。



3.Promise信任問題

??????前面說到,Promise在異步代碼中做的事情,一是作為“未來值”;二是反控制反轉(zhuǎn),接收“完成事件”。
??????但是我們還沒討論,Promise怎么應(yīng)對(duì)上一章講到的“回調(diào)模式中存在的信任問題”。
??????先回顧下回調(diào)中要面對(duì)的信任問題:回調(diào)調(diào)太早、回調(diào)調(diào)太晚或者根本不回調(diào)、回調(diào)次數(shù)過多或過少、沒傳遞該傳入的環(huán)境和參數(shù)、吞掉應(yīng)該報(bào)出的錯(cuò)誤和異常。

3.1 調(diào)用過早

??????如果回調(diào)函數(shù)可能被同步地調(diào)用,而使用者卻沒預(yù)料到,就可能出現(xiàn)競(jìng)態(tài)條件,搞出bug。
??????但Promise的定義,規(guī)定了即使是已經(jīng)決議的Promise,也無法被同步觀察到。也就是說,對(duì)一個(gè)Promise調(diào)用then(..)的時(shí)候,即使這個(gè)Promise已經(jīng)決議,提供給then(..)的回調(diào)也總會(huì)被異步調(diào)用。

3.2 調(diào)用過晚

??????Promise作為第三方中立的協(xié)商機(jī)制,會(huì)在 其創(chuàng)建對(duì)象 調(diào)用resolve或reject的時(shí)候,調(diào)度Promise對(duì)象上注冊(cè)的回調(diào)函數(shù)。所以這個(gè)層面上講,不用擔(dān)心異步操作結(jié)束后對(duì)方漏了哪個(gè)回調(diào),因?yàn)榛卣{(diào)這件事交給Promise去做了,我們可以信任它。

??????另外,也不用擔(dān)心同一個(gè)Promise對(duì)象上注冊(cè)的多個(gè)回調(diào)函數(shù)之間會(huì)互相延誤或阻止。一個(gè)Promise決議后,這個(gè)Promise上注冊(cè)的回調(diào)都會(huì)在下一個(gè)異步時(shí)機(jī)點(diǎn)上依次被立即調(diào)用,中間哪個(gè)回調(diào)要執(zhí)行異步操作,或者要拋異常,都不會(huì)延誤后面的回調(diào)被調(diào)用。

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

??????如果你對(duì)一個(gè)Promise注冊(cè)了一個(gè)完成回調(diào)和一個(gè)拒絕回調(diào),那么Promise在決議時(shí)總是會(huì)調(diào)用其中的一個(gè)。
??????那如果Promise本身永遠(yuǎn)不被決議呢?
??????可以利用Promise提供的race,Promise.race([p1, p2])返回一個(gè)新的Promise對(duì)象,p1和p2任意一個(gè)決議,新Promise對(duì)象就決議。我們可以設(shè)置p2的內(nèi)容為:設(shè)一個(gè)定時(shí)器,定時(shí)器到期后調(diào)用reject。這樣,p1如果一直不決議,Promise.race([p1, p2])也會(huì)在p2的定時(shí)器到期后決議。

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

??????“過少”已經(jīng)在前面講過可利用Promise.race防范。
??????“過多”,由于Promise的機(jī)制,Promise對(duì)象只能決議一次,不可以既調(diào)用resolve又調(diào)用reject,也不能多次調(diào)用,Promise對(duì)象只會(huì)接受第一次決議,后續(xù)的調(diào)用都會(huì)被忽略。

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

??????Promise的機(jī)制:調(diào)用resolve或reject時(shí)傳入的參數(shù)(只接收第一個(gè)參數(shù),多的會(huì)被忽略)就會(huì)成為決議值,不傳的話,決議值就是undefined。

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

??????我們已經(jīng)知道,直接調(diào)用reject可以使Promise決議到“拒絕”態(tài)。
??????但如果Promise在創(chuàng)建或在獲得結(jié)果的過程中出現(xiàn)了JavaScript異常,這個(gè)異常就會(huì)被捕獲,并使Promise對(duì)象決議到“拒絕”。

例:

let test = new Promise((resolve, reject) => {let a = b}) // b是未聲明的變量
test // Promise {<rejected>: ReferenceError: b is not defined

??????Promise的這個(gè)細(xì)節(jié),避免了“任務(wù)出錯(cuò)引起同步響應(yīng),不出錯(cuò)則會(huì)是異步的”的風(fēng)險(xiǎn),使我們不會(huì)遇到料想不到的競(jìng)態(tài)條件。
??????Promise甚至把JavaScript異常也變成了異步行為。
??????凡是Promise的決議都會(huì)異步執(zhí)行。

3.7 是可信任的Promise嗎

??????Promise并沒有完全擺脫回調(diào),只是我們之前是直接把回調(diào)丟給異步任務(wù),現(xiàn)在則是把回調(diào)丟給 根據(jù)異步任務(wù)得到的一個(gè)東西。
??????為什么這樣比回調(diào)更可信?怎么確定返回的東西就是個(gè)可信任的Promise呢?

??????在原生ES6 Promise實(shí)現(xiàn)中的解決方案就是Promise.resolve。
??????Promise.resolve(一個(gè)非Promise、非thenable的立即值) → 就會(huì)得到一個(gè)已經(jīng)決議到“完成”的Promise對(duì)象。

??????Promise.resolve(一個(gè)Promise值) → 就會(huì)得到里面那個(gè)Promise對(duì)象(新的Promise會(huì)替代掉原來那個(gè),等它決議才能得到結(jié)果。)

??????Promise.resolve(一個(gè)thenable值,它不是真的Promise,但有then方法) → Promise就會(huì)試圖展開這個(gè)thenable,直到最后提取出一個(gè)非thenable的最終值。

??????所謂的展開:
??????Promise調(diào)用這個(gè)thenable的then方法,給它傳入Promise自己的完成回調(diào)和失敗回調(diào)。
??????如果它調(diào)失敗回調(diào),那就決議到“拒絕”
??????如果它調(diào)成功回調(diào),那就決議到“完成”。
??????如果它調(diào)成功回調(diào),并且還傳參傳了個(gè)新的thenable,那就繼續(xù)調(diào)新的thenable的then方法。


展開.PNG

??????假如我們要調(diào)一個(gè)工具foo(..),但不確定得到的返回值是不是一個(gè)可信任的規(guī)范的promise,我們可以把它丟給Promise.resolve,Promise.resolve會(huì)替你過濾掉thenable值。
??????從語義上來講,Promise不會(huì)直接把一個(gè)thenable當(dāng)作決議值丟出去,而是把它看作一個(gè)“未來值”,等它決議(也就是等它調(diào)了傳入的成功回調(diào)或失敗回調(diào)),一直到它不會(huì)再?zèng)Q議出“未來值”而是決議出“普通值”,這個(gè)普通值才被視作決議結(jié)果。

??????比如:foo(42).then(回調(diào)),要是不確定foo會(huì)不會(huì)返回盜版Promise,可以這樣:Promise.resolve(foo(42)).then(回調(diào))

3.8 建立信任

??????前面的討論已經(jīng)講解了為什么Promise是可信任的,以及為什么對(duì)于構(gòu)建健壯可維護(hù)的應(yīng)用來說,建立信任很重要。
??????Promise模式通過可信任的語義把回調(diào)的控制反轉(zhuǎn) 反轉(zhuǎn)回來,我們把控制權(quán)放在了可信任的系統(tǒng)(Promise)中,這個(gè)系統(tǒng)設(shè)計(jì)的目的就是為了使異步編碼更清晰。



4.鏈?zhǔn)搅?/h3>

??????Promise并不只是一個(gè)單步執(zhí)行this-then-that操作的機(jī)制,還可以把多個(gè)Promise連接到一起來表示一系列的異步步驟。

??????Promise怎么做到串聯(lián)一系列異步步驟呢?
??????1.一個(gè)Promise的then方法不僅接收成功、失敗回調(diào),而且還會(huì)返回一個(gè)新的Promise,新的Promise表示then接收的回調(diào)的完成情況
??????2.如果then接收的回調(diào)返回的不是一個(gè)普通值而是一個(gè)“未來值”(Promise、thenable),未來值會(huì)被等待(展開),等到出結(jié)果,這個(gè)結(jié)果才會(huì)被當(dāng)作異步任務(wù)鏈中當(dāng)前環(huán)節(jié)的執(zhí)行結(jié)果,并被傳遞給這個(gè)“環(huán)節(jié)”的then方法所接收的回調(diào)。
??????3.then鏈中任務(wù)如果失敗(reject)或者拋出異常,那么其后續(xù)環(huán)節(jié)都會(huì)reject,當(dāng)然,也可以在出錯(cuò)環(huán)節(jié)的后面任一環(huán)節(jié)進(jìn)行捕捉,捕捉掉錯(cuò)誤之后,這一環(huán)節(jié)就算“正?!苯Y(jié)束,進(jìn)行了捕捉的環(huán)節(jié)的后續(xù)環(huán)節(jié)不會(huì)因?yàn)榍懊鏇]捕捉的錯(cuò)誤而eject了。

??????比起用回調(diào)寫出來的異步代碼,Promise鏈接異步任務(wù)的順序表達(dá)是個(gè)很大的進(jìn)步。

??????最后作者從語義上分析了一下Promise相關(guān)的幾個(gè)術(shù)語:
??????1.為什么Promise構(gòu)造器傳遞給被包裹的任務(wù)的兩個(gè)回調(diào)函數(shù),通常被分別稱為resolve和reject呢?
??????答:reject沒什么說的,是失敗的時(shí)候調(diào)的。resolve被叫做resolve而不叫做fllfilled,是因?yàn)閞esolve方法可能接收到一個(gè)未來值,未來值的展開結(jié)果可能是reject的。所以用"resolve"這個(gè)名稱更恰當(dāng)。
??????2.then方法接收的回調(diào)呢?叫什么名字合適?
??????答:因?yàn)樗鼈z永遠(yuǎn)是分別處理任務(wù)的“完成”和“失敗”,所以叫做fulfilled和rejected就很合適了。
??????而且在ES6規(guī)范里,它們也被叫做onFulfilled和onRejected,實(shí)至名歸。



5.錯(cuò)誤處理

??????Promise的錯(cuò)誤處理對(duì)初接觸的人來說不是很直觀,一個(gè)任務(wù)決議后,其fulfilled回調(diào)執(zhí)行失敗,不是由注冊(cè)在該任務(wù)上的rejected回調(diào)去接收錯(cuò)誤信息,而是該任務(wù)的下一環(huán)任務(wù)上注冊(cè)的rejected回調(diào)去接收。
??????雖然這樣其實(shí)是合理的,但是乍一看,有點(diǎn)令人難以理解,然后一不小心會(huì)因?yàn)槔斫庥姓`,寫出bug。

??????有些開發(fā)者提出,在實(shí)際開發(fā)中,可以在then后面加catch,以保證then接收的回調(diào)函數(shù)如果出錯(cuò)了也能被后面那個(gè)catch抓到。
??????但假如catch接收的錯(cuò)誤處理方法自身出錯(cuò)了呢?
??????這種寫法還是不能保證整條鏈任意環(huán)節(jié)出的錯(cuò)都能被捕獲。

??????還有些辦法可以判斷Promise任務(wù)鏈中是否有錯(cuò)誤“從始至終未被捕獲”。
??????如:設(shè)置定時(shí)器,一定時(shí)間內(nèi)拒絕態(tài)的Promise沒有被注冊(cè)錯(cuò)誤處理函數(shù),就當(dāng)它是“從始至終未被捕獲”,當(dāng)然,這樣肯定有點(diǎn)問題。
??????還有讓瀏覽器對(duì)Promise變量回收時(shí)判斷它是否處于拒絕態(tài),如果是說明它“從始至終未被處理”,但這樣不能兼顧到因?yàn)楦鞣N原因變量沒釋放的情況。

??????接下來作者講了理論上“更好的Promise”該擁有的特性,“理想中的Promise”在決議到拒絕后,要默認(rèn)報(bào)告未被處理的拒絕(在決議后的下一個(gè)異步時(shí)間點(diǎn)),除非它被顯式調(diào)用了defer,或者它已經(jīng)注冊(cè)了一個(gè)錯(cuò)誤處理函數(shù)。
??????(我沒懂什么叫“報(bào)告”,控制臺(tái)報(bào)錯(cuò)算“報(bào)告”嗎?如果是的話現(xiàn)在不就已經(jīng)是了嗎,沒catch的錯(cuò)誤會(huì)報(bào)到控制臺(tái)??赡墁F(xiàn)在我接觸到的就是優(yōu)化過的Promise。)


6.Promise模式

??????除了Promise鏈這樣的順序模式,基于Promise構(gòu)建的異步模式抽象還有很多變體。

Promise.all([...])

??????在經(jīng)典的編程術(shù)語中,門(gate)是這樣一種機(jī)制:要等待兩個(gè)或更多并行 / 并發(fā)的任務(wù)都完成才能繼續(xù)。它們的完成順序并不重要,但是必須都要完成,門才能打開并讓流程控制繼續(xù)。
??????在Promise API中,這種模式被稱為all([...])
??????Promise.all([...])返回的主promise在且僅在所有成員promise都完成后才會(huì)完成。任一成員promise被拒絕,主promise就會(huì)被拒絕,并丟棄其它成員promise的決議結(jié)果。

Promise.race([...])

??????多個(gè)任務(wù)并發(fā)運(yùn)行,只響應(yīng)“第一個(gè)跨過終點(diǎn)線的任務(wù)”,這種模式傳統(tǒng)上稱為門閂,但在Promise中稱為競(jìng)態(tài)。(注意不要混淆這里的“競(jìng)態(tài)”,和相當(dāng)于bug的“競(jìng)態(tài)條件”)
一旦有任何一個(gè)成員Promise決議為完成,Promise.race([...])就會(huì)完成。

??????那么從行為上看,那些被丟棄的成員promise會(huì)發(fā)生什么呢?
??????假如一個(gè)成員任務(wù)內(nèi)部保留著一些資源,然后這個(gè)成員任務(wù)被主任務(wù)忽略了,那么是不是應(yīng)該有什么api可以用于主動(dòng)釋放這些資源或者取消可能產(chǎn)生的副作用?
??????(這段話看不懂,首先,“一個(gè)Promise內(nèi)保留了一些資源”是什么意思,指的是Promise里邊包裹的任務(wù)所用到的資源嗎?這些資源會(huì)在決議之后釋放吧?而且這個(gè)時(shí)候主動(dòng)釋放資源,是不打算讓這個(gè)任務(wù)走下去了嗎?)
??????然后作者提到一種未來的Promise可以有的api:finally,用于接收在主promise任務(wù)決議后進(jìn)行清理的回調(diào)。

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

除了原生Promise提供的all和race,還有幾種其它的變體。
none([...])
any([...])
first([...])
(其實(shí)我看不懂a(chǎn)ny和first的邏輯有什么區(qū)別)
有些Promise抽象庫提供了這些支持,你也可以自己實(shí)現(xiàn)。
小練習(xí):自己實(shí)現(xiàn)一下first()

并發(fā)迭代

??????假如對(duì)一堆異步任務(wù)要做同樣的處理,而且這些處理不是同步操作,就需要寫個(gè)類似forEach(但forEach是用于同步操作的)的迭代工具方法。
??????書里寫了個(gè)實(shí)現(xiàn)的示例,不抄錄了。



7.Promise API概述

??????Promise構(gòu)造器接收一個(gè)函數(shù)參數(shù),這個(gè)函數(shù)表示Promise對(duì)象的內(nèi)容,函數(shù)會(huì)在new Promise語句執(zhí)行到的時(shí)候被立即同步調(diào)用。
??????其它略。



8 Promise局限性

8.1 順序錯(cuò)誤處理

??????作者說,一個(gè)Promise鏈,如果其中一個(gè)環(huán)節(jié)出了錯(cuò)誤并且被錯(cuò)誤處理函數(shù)捉掉了,那么在鏈底你就覺察不到鏈中其實(shí)有環(huán)節(jié)失敗。類似try catch,error被catch了以后就不會(huì)再往上拋,你就不知道底下其實(shí)發(fā)生過錯(cuò)誤。
??????(我看不出來為什么這也算缺陷。。不過作者說,這算是一個(gè)限制)

8.2 單一值

??????如果一個(gè)Promise需要返回多個(gè)值,只能把它做成個(gè)對(duì)象或者數(shù)組,不過也可以思考下是不是里邊的邏輯應(yīng)該拆分了。

8.3 單決議

??????Promise適用于只決議一次的異步。像事件、數(shù)據(jù)流這樣的模式,不適合直接使用Promise,起碼得對(duì)Promise再包一層邏輯才能用。

8.4 慣性

??????討論了一些把原有的回調(diào)風(fēng)格的代碼修改成Promise風(fēng)格的代碼所要做的事,介紹了一些起轉(zhuǎn)換作用的工具函數(shù)

8.5 無法取消的Promise

??????Promise一旦創(chuàng)建就無法取消。如果允許Promise的取消,那么Promise的一個(gè)消費(fèi)者就可以影響其它消費(fèi)者查看這個(gè)Promise,這違背了未來值的可信任性(外部不變性)。
??????單獨(dú)的一個(gè)Promise并不是一個(gè)真正的流程控制機(jī)制,這就是為什么Promise取消總是讓人感覺很別扭。相比之下,集合在一起的Promise構(gòu)成的鏈(可以稱之為一個(gè)“序列”),就是一個(gè)流程控制的表達(dá),將取消定義在這個(gè)抽象層次上是合適的。

8.6 Promise性能

??????Promise與不可信任的裸回調(diào)相比會(huì)稍慢一些。

最后編輯于
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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