本文適用的讀者
本文寫給有一定Promise使用經(jīng)驗(yàn)的人,如果你還沒有使用過Promise,這篇文章可能不適合你,建議先了解Promise的使用
Promise標(biāo)準(zhǔn)解讀
只有一個(gè)then方法,沒有catch,race,all等方法,甚至沒有構(gòu)造函數(shù)
Promise標(biāo)準(zhǔn)中僅指定了Promise對(duì)象的then方法的行為,其它一切我們常見的方法/函數(shù)都并沒有指定,包括catch,race,all等常用方法,甚至也沒有指定該如何構(gòu)造出一個(gè)Promise對(duì)象,另外then也沒有一般實(shí)現(xiàn)中(Q, $q等)所支持的第三個(gè)參數(shù),一般稱onProgress
then方法返回一個(gè)新的Promise
Promise的then方法返回一個(gè)新的Promise,而不是返回this,此處在下文會(huì)有更多解釋
promise2=promise1.then(alert)promise2!=promise1// true
不同Promise的實(shí)現(xiàn)需要可以相互調(diào)用(interoperable)
Promise的初始狀態(tài)為pending,它可以由此狀態(tài)轉(zhuǎn)換為fulfilled(本文為了一致把此狀態(tài)叫做resolved)或者rejected,一旦狀態(tài)確定,就不可以再次轉(zhuǎn)換為其它狀態(tài),狀態(tài)確定的過程稱為settle
一步一步實(shí)現(xiàn)一個(gè)Promise
下面我們就來一步一步實(shí)現(xiàn)一個(gè)Promise
構(gòu)造函數(shù)
因?yàn)闃?biāo)準(zhǔn)并沒有指定如何構(gòu)造一個(gè)Promise對(duì)象,所以我們同樣以目前一般Promise實(shí)現(xiàn)中通用的方法來構(gòu)造一個(gè)Promise對(duì)象,也是ES6原生Promise里所使用的方式,即:
// Promise構(gòu)造函數(shù)接收一個(gè)executor函數(shù),executor函數(shù)執(zhí)行完同步或異步操作后,調(diào)用它的兩個(gè)參數(shù)resolve和rejectvarpromise=newPromise(function(resolve,reject){/*如果操作成功,調(diào)用resolve并傳入value如果操作失敗,調(diào)用reject并傳入reason*/})
我們先實(shí)現(xiàn)構(gòu)造函數(shù)的框架如下:
functionPromise(executor){varself=thisself.status='pending'// Promise當(dāng)前的狀態(tài)self.data=undefined// Promise的值self.onResolvedCallback=[]// Promise resolve時(shí)的回調(diào)函數(shù)集,因?yàn)樵赑romise結(jié)束之前有可能有多個(gè)回調(diào)添加到它上面self.onRejectedCallback=[]// Promise reject時(shí)的回調(diào)函數(shù)集,因?yàn)樵赑romise結(jié)束之前有可能有多個(gè)回調(diào)添加到它上面executor(resolve,reject)// 執(zhí)行executor并傳入相應(yīng)的參數(shù)}
上面的代碼基本實(shí)現(xiàn)了Promise構(gòu)造函數(shù)的主體,但目前還有兩個(gè)問題:
我們給executor函數(shù)傳了兩個(gè)參數(shù):resolve和reject,這兩個(gè)參數(shù)目前還沒有定義
executor有可能會(huì)出錯(cuò)(throw),類似下面這樣,而如果executor出錯(cuò),Promise應(yīng)該被其throw出的值reject:
newPromise(function(resolve,reject){throw2})
所以我們需要在構(gòu)造函數(shù)里定義resolve和reject這兩個(gè)函數(shù):
functionPromise(executor){varself=thisself.status='pending'// Promise當(dāng)前的狀態(tài)self.data=undefined// Promise的值self.onResolvedCallback=[]// Promise resolve時(shí)的回調(diào)函數(shù)集,因?yàn)樵赑romise結(jié)束之前有可能有多個(gè)回調(diào)添加到它上面self.onRejectedCallback=[]// Promise reject時(shí)的回調(diào)函數(shù)集,因?yàn)樵赑romise結(jié)束之前有可能有多個(gè)回調(diào)添加到它上面functionresolve(value){// TODO}functionreject(reason){// TODO}try{// 考慮到執(zhí)行executor的過程中有可能出錯(cuò),所以我們用try/catch塊給包起來,并且在出錯(cuò)后以catch到的值reject掉這個(gè)Promiseexecutor(resolve,reject)// 執(zhí)行executor}catch(e){reject(e)}}
有人可能會(huì)問,resolve和reject這兩個(gè)函數(shù)能不能不定義在構(gòu)造函數(shù)里呢?考慮到我們?cè)趀xecutor函數(shù)里是以resolve(value),reject(reason)的形式調(diào)用的這兩個(gè)函數(shù),而不是以resolve.call(promise, value),reject.call(promise, reason)這種形式調(diào)用的,所以這兩個(gè)函數(shù)在調(diào)用時(shí)的內(nèi)部也必然有一個(gè)隱含的this,也就是說,要么這兩個(gè)函數(shù)是經(jīng)過bind后傳給了executor,要么它們定義在構(gòu)造函數(shù)的內(nèi)部,使用self來訪問所屬的Promise對(duì)象。所以如果我們想把這兩個(gè)函數(shù)定義在構(gòu)造函數(shù)的外部,確實(shí)是可以這么寫的:
functionresolve(){// TODO}functionreject(){// TODO}functionPromise(executor){try{executor(resolve.bind(this),reject.bind(this))}catch(e){reject.bind(this)(e)}}
但是眾所周知,bind也會(huì)返回一個(gè)新的函數(shù),這么一來還是相當(dāng)于每個(gè)Promise對(duì)象都有一對(duì)屬于自己的resolve和reject函數(shù),就跟寫在構(gòu)造函數(shù)內(nèi)部沒什么區(qū)別了,所以我們就直接把這兩個(gè)函數(shù)定義在構(gòu)造函數(shù)里面了。不過話說回來,如果瀏覽器對(duì)bind的所優(yōu)化,使用后一種形式應(yīng)該可以提升一下內(nèi)存使用效率。
另外我們這里的實(shí)現(xiàn)并沒有考慮隱藏this上的變量,這使得這個(gè)Promise的狀態(tài)可以在executor函數(shù)外部被改變,在一個(gè)靠譜的實(shí)現(xiàn)里,構(gòu)造出的Promise對(duì)象的狀態(tài)和最終結(jié)果應(yīng)當(dāng)是無法從外部更改的。
接下來,我們實(shí)現(xiàn)resolve和reject這兩個(gè)函數(shù)
functionPromise(executor){// ...functionresolve(value){if(self.status==='pending'){self.status='resolved'self.data=valuefor(vari=0;i
基本上就是在判斷狀態(tài)為pending之后把狀態(tài)改為相應(yīng)的值,并把對(duì)應(yīng)的value和reason存在self的data屬性上面,之后執(zhí)行相應(yīng)的回調(diào)函數(shù),邏輯很簡單,這里就不多解釋了。
then方法
Promise對(duì)象有一個(gè)then方法,用來注冊(cè)在這個(gè)Promise狀態(tài)確定后的回調(diào),很明顯,then方法需要寫在原型鏈上。then方法會(huì)返回一個(gè)Promise,關(guān)于這一點(diǎn),Promise/A+標(biāo)準(zhǔn)并沒有要求返回的這個(gè)Promise是一個(gè)新的對(duì)象,但在Promise/A標(biāo)準(zhǔn)中,明確規(guī)定了then要返回一個(gè)新的對(duì)象,目前的Promise實(shí)現(xiàn)中then幾乎都是返回一個(gè)新的Promise(詳情)對(duì)象,所以在我們的實(shí)現(xiàn)中,也讓then返回一個(gè)新的Promise對(duì)象。
關(guān)于這一點(diǎn),我認(rèn)為標(biāo)準(zhǔn)中是有一點(diǎn)矛盾的:
標(biāo)準(zhǔn)中說,如果promise2 = promise1.then(onResolved, onRejected)里的onResolved/onRejected返回一個(gè)Promise,則promise2直接取這個(gè)Promise的狀態(tài)和值為己用,但考慮如下代碼:
promise2=promise1.then(functionfoo(value){returnPromise.reject(3)})
此處如果foo運(yùn)行了,則promise1的狀態(tài)必然已經(jīng)確定且為resolved,如果then返回了this(即promise2 === promise1),說明promise2和promise1是同一個(gè)對(duì)象,而此時(shí)promise1/2的狀態(tài)已經(jīng)確定,沒有辦法再取Promise.reject(3)的狀態(tài)和結(jié)果為己用,因?yàn)镻romise的狀態(tài)確定后就不可再轉(zhuǎn)換為其它狀態(tài)。
另外每個(gè)Promise對(duì)象都可以在其上多次調(diào)用then方法,而每次調(diào)用then返回的Promise的狀態(tài)取決于那一次調(diào)用then時(shí)傳入?yún)?shù)的返回值,所以then不能返回this,因?yàn)閠hen每次返回的Promise的結(jié)果都有可能不同。
下面我們來實(shí)現(xiàn)then方法:
// then方法接收兩個(gè)參數(shù),onResolved,onRejected,分別為Promise成功或失敗后的回調(diào)Promise.prototype.then=function(onResolved,onRejected){varself=thisvarpromise2// 根據(jù)標(biāo)準(zhǔn),如果then的參數(shù)不是function,則我們需要忽略它,此處以如下方式處理onResolved=typeofonResolved==='function'?onResolved:function(v){}onRejected=typeofonRejected==='function'?onRejected:function(r){}if(self.status==='resolved'){returnpromise2=newPromise(function(resolve,reject){})}if(self.status==='rejected'){returnpromise2=newPromise(function(resolve,reject){})}if(self.status==='pending'){returnpromise2=newPromise(function(resolve,reject){})}}
Promise總共有三種可能的狀態(tài),我們分三個(gè)if塊來處理,在里面分別都返回一個(gè)new Promise。
根據(jù)標(biāo)準(zhǔn),我們知道,對(duì)于如下代碼,promise2的值取決于then里面函數(shù)的返回值:
promise2=promise1.then(function(value){return4},function(reason){thrownewError('sth went wrong')})
如果promise1被resolve了,promise2的將被4 resolve,如果promise1被reject了,promise2將被new Error('sth went wrong') reject,更多復(fù)雜的情況不再詳述。
所以,我們需要在then里面執(zhí)行onResolved或者onRejected,并根據(jù)返回值(標(biāo)準(zhǔn)中記為x)來確定promise2的結(jié)果,并且,如果onResolved/onRejected返回的是一個(gè)Promise,promise2將直接取這個(gè)Promise的結(jié)果:
Promise.prototype.then=function(onResolved,onRejected){varself=thisvarpromise2// 根據(jù)標(biāo)準(zhǔn),如果then的參數(shù)不是function,則我們需要忽略它,此處以如下方式處理onResolved=typeofonResolved==='function'?onResolved:function(value){}onRejected=typeofonRejected==='function'?onRejected:function(reason){}if(self.status==='resolved'){// 如果promise1(此處即為this/self)的狀態(tài)已經(jīng)確定并且是resolved,我們調(diào)用onResolved// 因?yàn)榭紤]到有可能throw,所以我們將其包在try/catch塊里returnpromise2=newPromise(function(resolve,reject){try{varx=onResolved(self.data)if(xinstanceofPromise){// 如果onResolved的返回值是一個(gè)Promise對(duì)象,直接取它的結(jié)果做為promise2的結(jié)果x.then(resolve,reject)}resolve(x)// 否則,以它的返回值做為promise2的結(jié)果}catch(e){reject(e)// 如果出錯(cuò),以捕獲到的錯(cuò)誤做為promise2的結(jié)果}})}// 此處與前一個(gè)if塊的邏輯幾乎相同,區(qū)別在于所調(diào)用的是onRejected函數(shù),就不再做過多解釋if(self.status==='rejected'){returnpromise2=newPromise(function(resolve,reject){try{varx=onRejected(self.data)if(xinstanceofPromise){x.then(resolve,reject)}}catch(e){reject(e)}})}if(self.status==='pending'){// 如果當(dāng)前的Promise還處于pending狀態(tài),我們并不能確定調(diào)用onResolved還是onRejected,// 只能等到Promise的狀態(tài)確定后,才能確實(shí)如何處理。// 所以我們需要把我們的**兩種情況**的處理邏輯做為callback放入promise1(此處即this/self)的回調(diào)數(shù)組里// 邏輯本身跟第一個(gè)if塊內(nèi)的幾乎一致,此處不做過多解釋returnpromise2=newPromise(function(resolve,reject){self.onResolvedCallback.push(function(value){try{varx=onResolved(self.data)if(xinstanceofPromise){x.then(resolve,reject)}}catch(e){reject(e)}})self.onRejectedCallback.push(function(reason){try{varx=onRejected(self.data)if(xinstanceofPromise){x.then(resolve,reject)}}catch(e){reject(e)}})})}}// 為了下文方便,我們順便實(shí)現(xiàn)一個(gè)catch方法Promise.prototype.catch=function(onRejected){returnthis.then(null,onRejected)}
至此,我們基本實(shí)現(xiàn)了Promise標(biāo)準(zhǔn)中所涉及到的內(nèi)容,但還有幾個(gè)問題:
不同的Promise實(shí)現(xiàn)之間需要無縫的可交互,即Q的Promise,ES6的Promise,和我們實(shí)現(xiàn)的Promise之間以及其它的Promise實(shí)現(xiàn),應(yīng)該并且是有必要無縫相互調(diào)用的,比如:
// 此處用MyPromise來代表我們實(shí)現(xiàn)的PromisenewMyPromise(function(resolve,reject){// 我們實(shí)現(xiàn)的PromisesetTimeout(function(){resolve(42)},2000)}).then(function(){returnnewPromise.reject(2)// ES6的Promise}).then(function(){returnQ.all([// Q的PromisenewMyPromise(resolve=>resolve(8)),// 我們實(shí)現(xiàn)的PromisenewPromise.resolve(9),// ES6的PromiseQ.resolve(9)// Q的Promise])})
我們前面實(shí)現(xiàn)的代碼并沒有處理這樣的邏輯,我們只判斷了onResolved/onRejected的返回值是否為我們實(shí)現(xiàn)的Promise的實(shí)例,并沒有做任何其它的判斷,所以上面這樣的代碼目前是沒有辦法在我們的Promise里正確運(yùn)行的。
下面這樣的代碼目前也是沒辦法處理的:
newPromise(resolve=>resolve(8)).then().then().then(functionfoo(value){alert(value)})
正確的行為應(yīng)該是alert出8,而如果拿我們的Promise,運(yùn)行上述代碼,將會(huì)alert出undefined。這種行為稱為穿透,即8這個(gè)值會(huì)穿透兩個(gè)then(說Promise更為準(zhǔn)確)到達(dá)最后一個(gè)then里的foo函數(shù)里,成為它的實(shí)參,最終將會(huì)alert出8。
下面我們首先處理簡單的情況,值的穿透
Promise值的穿透
通過觀察,會(huì)發(fā)現(xiàn)我們希望下面這段代碼
newPromise(resolve=>resolve(8)).then().catch().then(function(value){alert(value)})
跟下面這段代碼的行為是一樣的
newPromise(resolve=>resolve(8)).then(function(value){returnvalue}).catch(function(reason){throwreason}).then(function(value){alert(value)})
所以如果想要把then的實(shí)參留空且讓值可以穿透到后面,意味著then的兩個(gè)參數(shù)的默認(rèn)值分別為function(value) {return value},function(reason) {throw reason}。
所以我們只需要把then里判斷onResolved和onRejected的部分改成如下即可:
onResolved=typeofonResolved==='function'?onResolved:function(value){returnvalue}onRejected=typeofonRejected==='function'?onRejected:function(reason){throwreason}
于是Promise神奇的值的穿透也沒有那么黑魔法,只不過是then默認(rèn)參數(shù)就是把值往后傳或者拋
不同Promise的交互
關(guān)于不同Promise間的交互,其實(shí)標(biāo)準(zhǔn)里是有說明的,其中詳細(xì)指定了如何通過then的實(shí)參返回的值來決定promise2的狀態(tài),我們只需要按照標(biāo)準(zhǔn)把標(biāo)準(zhǔn)的內(nèi)容轉(zhuǎn)成代碼即可。
這里簡單解釋一下標(biāo)準(zhǔn):
即我們要把onResolved/onRejected的返回值,x,當(dāng)成一個(gè)可能是Promise的對(duì)象,也即標(biāo)準(zhǔn)里所說的thenable,并以最保險(xiǎn)的方式調(diào)用x上的then方法,如果大家都按照標(biāo)準(zhǔn)實(shí)現(xiàn),那么不同的Promise之間就可以交互了。而標(biāo)準(zhǔn)為了保險(xiǎn)起見,即使x返回了一個(gè)帶有then屬性但并不遵循Promise標(biāo)準(zhǔn)的對(duì)象(比如說這個(gè)x把它then里的兩個(gè)參數(shù)都調(diào)用了,同步或者異步調(diào)用(PS,原則上then的兩個(gè)參數(shù)需要異步調(diào)用,下文會(huì)講到),或者是出錯(cuò)后又調(diào)用了它們,或者then根本不是一個(gè)函數(shù)),也能盡可能正確處理。
關(guān)于為何需要不同的Promise實(shí)現(xiàn)能夠相互交互,我想原因應(yīng)該是顯然的,Promise并不是JS一早就有的標(biāo)準(zhǔn),不同第三方的實(shí)現(xiàn)之間是并不相互知曉的,如果你使用的某一個(gè)庫中封裝了一個(gè)Promise實(shí)現(xiàn),想象一下如果它不能跟你自己使用的Promise實(shí)現(xiàn)交互的場景。。。
建議各位對(duì)照著標(biāo)準(zhǔn)閱讀以下代碼,因?yàn)闃?biāo)準(zhǔn)對(duì)此說明的非常詳細(xì),所以你應(yīng)該能夠在任意一個(gè)Promise實(shí)現(xiàn)中找到類似的代碼:
/*resolvePromise函數(shù)即為根據(jù)x的值來決定promise2的狀態(tài)的函數(shù)也即標(biāo)準(zhǔn)中的[Promise Resolution Procedure](https://promisesaplus.com/#point-47)x為`promise2 = promise1.then(onResolved, onRejected)`里`onResolved/onRejected`的返回值`resolve`和`reject`實(shí)際上是`promise2`的`executor`的兩個(gè)實(shí)參,因?yàn)楹茈y掛在其它的地方,所以一并傳進(jìn)來。相信各位一定可以對(duì)照標(biāo)準(zhǔn)把標(biāo)準(zhǔn)轉(zhuǎn)換成代碼,這里就只標(biāo)出代碼在標(biāo)準(zhǔn)中對(duì)應(yīng)的位置,只在必要的地方做一些解釋*/functionresolvePromise(promise2,x,resolve,reject){varthenvarthenCalledOrThrow=falseif(promise2===x){// 對(duì)應(yīng)標(biāo)準(zhǔn)2.3.1節(jié)returnreject(newTypeError('Chaining cycle detected for promise!'))}if(xinstanceofPromise){// 對(duì)應(yīng)標(biāo)準(zhǔn)2.3.2節(jié)// 如果x的狀態(tài)還沒有確定,那么它是有可能被一個(gè)thenable決定最終狀態(tài)和值的// 所以這里需要做一下處理,而不能一概的以為它會(huì)被一個(gè)“正常”的值resolveif(x.status==='pending'){x.then(function(value){resolvePromise(promise2,value,resolve,reject)},reject)}else{// 但如果這個(gè)Promise的狀態(tài)已經(jīng)確定了,那么它肯定有一個(gè)“正常”的值,而不是一個(gè)thenable,所以這里直接取它的狀態(tài)x.then(resolve,reject)}return}if((x!==null)&&((typeofx==='object')||(typeofx==='function'))){// 2.3.3try{// 2.3.3.1 因?yàn)閤.then有可能是一個(gè)getter,這種情況下多次讀取就有可能產(chǎn)生副作用// 即要判斷它的類型,又要調(diào)用它,這就是兩次讀取then=x.thenif(typeofthen==='function'){// 2.3.3.3then.call(x,functionrs(y){// 2.3.3.3.1if(thenCalledOrThrow)return// 2.3.3.3.3 即這三處誰選執(zhí)行就以誰的結(jié)果為準(zhǔn)thenCalledOrThrow=truereturnresolvePromise(promise2,y,resolve,reject)// 2.3.3.3.1},functionrj(r){// 2.3.3.3.2if(thenCalledOrThrow)return// 2.3.3.3.3 即這三處誰選執(zhí)行就以誰的結(jié)果為準(zhǔn)thenCalledOrThrow=truereturnreject(r)})}else{// 2.3.3.4resolve(x)}}catch(e){// 2.3.3.2if(thenCalledOrThrow)return// 2.3.3.3.3 即這三處誰選執(zhí)行就以誰的結(jié)果為準(zhǔn)thenCalledOrThrow=truereturnreject(e)}}else{// 2.3.4resolve(x)}}
然后我們使用這個(gè)函數(shù)的調(diào)用替換then里幾處判斷x是否為Promise對(duì)象的位置即可,見下方完整代碼。
最后,我們剛剛說到,原則上,promise.then(onResolved, onRejected)里的這兩相函數(shù)需要異步調(diào)用,關(guān)于這一點(diǎn),標(biāo)準(zhǔn)里也有說明:
In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack.
所以我們需要對(duì)我們的代碼做一點(diǎn)變動(dòng),即在四個(gè)地方加上setTimeout(fn, 0),這點(diǎn)會(huì)在完整的代碼中注釋,請(qǐng)各位自行發(fā)現(xiàn)。
事實(shí)上,即使你不參照標(biāo)準(zhǔn),最終你在自測試時(shí)也會(huì)發(fā)現(xiàn)如果then的參數(shù)不以異步的方式調(diào)用,有些情況下Promise會(huì)不按預(yù)期的方式行為,通過不斷的自測,最終你必然會(huì)讓then的參數(shù)異步執(zhí)行,讓executor函數(shù)立即執(zhí)行。本人在一開始實(shí)現(xiàn)Promise時(shí)就沒有參照標(biāo)準(zhǔn),而是自己憑經(jīng)驗(yàn)測試,最終發(fā)現(xiàn)的這個(gè)問題。
至此,我們就實(shí)現(xiàn)了一個(gè)的Promise,完整代碼如下:
try{module.exports=Promise}catch(e){}functionPromise(executor){varself=thisself.status='pending'self.onResolvedCallback=[]self.onRejectedCallback=[]functionresolve(value){if(valueinstanceofPromise){returnvalue.then(resolve,reject)}setTimeout(function(){// 異步執(zhí)行所有的回調(diào)函數(shù)if(self.status==='pending'){self.status='resolved'self.data=valuefor(vari=0;i
測試
如何確定我們實(shí)現(xiàn)的Promise符合標(biāo)準(zhǔn)呢?Promise有一個(gè)配套的測試腳本,只需要我們?cè)谝粋€(gè)CommonJS的模塊中暴露一個(gè)deferred方法(即exports.deferred方法),就可以了,代碼見上述代碼的最后。然后執(zhí)行如下代碼即可執(zhí)行測試:
npm i -g promises-aplus-testspromises-aplus-tests Promise.js
關(guān)于Promise的其它問題
Promise的性能問題
可能各位看官會(huì)覺得奇怪,Promise能有什么性能問題呢?并沒有大量的計(jì)算啊,幾乎都是處理邏輯的代碼。
理論上說,不能叫做“性能問題”,而只是有可能出現(xiàn)的延遲問題。什么意思呢,記得剛剛我們說需要把4塊代碼包在setTimeout里吧,先考慮如下代碼:
varstart=+newDate()functionfoo(){setTimeout(function(){console.log('setTimeout')if((+newDate)-start<1000){foo()}})}foo()
運(yùn)行上面的代碼,會(huì)打印出多少次'setTimeout'呢,各位可以自己試一下,不出意外的話,應(yīng)該是250次左右,我剛剛運(yùn)行了一次,是241次。這說明,上述代碼中兩次setTimeout運(yùn)行的時(shí)間間隔約是4ms(另外,setInterval也是一樣的),實(shí)事上,這正是瀏覽器兩次Event Loop之間的時(shí)間間隔,相關(guān)標(biāo)準(zhǔn)各位可以自行查閱。另外,在Node中,這個(gè)時(shí)間間隔跟瀏覽器不一樣,經(jīng)過我的測試,是1ms。
單單一個(gè)4ms的延遲可能在一般的web應(yīng)用中并不會(huì)有什么問題,但是考慮極端情況,我們有20個(gè)Promise鏈?zhǔn)秸{(diào)用,加上代碼運(yùn)行的時(shí)間,那么這個(gè)鏈?zhǔn)秸{(diào)用的第一行代碼跟最后一行代碼的運(yùn)行很可能會(huì)超過100ms,如果這之間沒有對(duì)UI有任何更新的話,雖然本質(zhì)上沒有什么性能問題,但可能會(huì)造成一定的卡頓或者閃爍,雖然在web應(yīng)用中這種情形并不常見,但是在Node應(yīng)用中,確實(shí)是有可能出現(xiàn)這樣的case的,所以一個(gè)能夠應(yīng)用于生產(chǎn)環(huán)境的實(shí)現(xiàn)有必要把這個(gè)延遲消除掉。在Node中,我們可以調(diào)用process.nextTick或者setImmediate(Q就是這么做的),在瀏覽器中具體如何做,已經(jīng)超出了本文的討論范圍,總的來說,就是我們需要實(shí)現(xiàn)一個(gè)函數(shù),行為跟setTimeout一樣,但它需要異步且盡早的調(diào)用所有已經(jīng)加入隊(duì)列的函數(shù),這里有一個(gè)實(shí)現(xiàn)。
如何停止一個(gè)Promise鏈?
在一些場景下,我們可能會(huì)遇到一個(gè)較長的Promise鏈?zhǔn)秸{(diào)用,在某一步中出現(xiàn)的錯(cuò)誤讓我們完全沒有必要去運(yùn)行鏈?zhǔn)秸{(diào)用后面所有的代碼,類似下面這樣(此處略去了then/catch里的函數(shù)):
new Promise(function(resolve, reject) {? resolve(42)})? .then(function(value) {? ? // "Big ERROR!!!"? })? .catch()? .then()? .then()? .catch()? .then()
假設(shè)這個(gè)Big ERROR!!!的出現(xiàn)讓我們完全沒有必要運(yùn)行后面所有的代碼了,但鏈?zhǔn)秸{(diào)用的后面即有catch,也有then,無論我們是return還是throw,都不可避免的會(huì)進(jìn)入某一個(gè)catch或then里面,那有沒有辦法讓這個(gè)鏈?zhǔn)秸{(diào)用在Big ERROR!!!的后面就停掉,完全不去執(zhí)行鏈?zhǔn)秸{(diào)用后面所有回調(diào)函數(shù)呢?
一開始遇到這個(gè)問題的時(shí)候我也百思不得其解,在網(wǎng)上搜遍了也沒有結(jié)果,有人說可以在每個(gè)catch里面判斷Error的類型,如果自己處理不了就接著throw,也有些其它辦法,但總是要對(duì)現(xiàn)有代碼進(jìn)行一些改動(dòng)并且所有的地方都要遵循這些約定,甚是麻煩。
然而當(dāng)我從一個(gè)實(shí)現(xiàn)者的角度看問題時(shí),確實(shí)找到了答案,就是在發(fā)生Big ERROR后return一個(gè)Promise,但這個(gè)Promise的executor函數(shù)什么也不做,這就意味著這個(gè)Promise將永遠(yuǎn)處于pending狀態(tài),由于then返回的Promise會(huì)直接取這個(gè)永遠(yuǎn)處于pending狀態(tài)的Promise的狀態(tài),于是返回的這個(gè)Promise也將一直處于pending狀態(tài),后面的代碼也就一直不會(huì)執(zhí)行了,具體代碼如下:
newPromise(function(resolve,reject){resolve(42)}).then(function(value){// "Big ERROR!!!"returnnewPromise(function(){})}).catch().then().then().catch().then()
這種方式看起來有些山寨,它也確實(shí)解決了問題。但它引入的一個(gè)新問題就是鏈?zhǔn)秸{(diào)用后面的所有回調(diào)函數(shù)都無法被垃圾回收器回收(在一個(gè)靠譜的實(shí)現(xiàn)里,Promise應(yīng)該在執(zhí)行完所有回調(diào)后刪除對(duì)所有回調(diào)函數(shù)的引用以讓它們能被回收,在前文的實(shí)現(xiàn)里,為了減少復(fù)雜度,并沒有做這種處理),但如果我們不使用匿名函數(shù),而是使用函數(shù)定義或者函數(shù)變量的話,在需要多次執(zhí)行的Promise鏈中,這些函數(shù)也都只有一份在內(nèi)存中,不被回收也是可以接受的。
我們可以將返回一個(gè)什么也不做的Promise封裝成一個(gè)有語義的函數(shù),以增加代碼的可讀性:
Promise.cancel=Promise.stop=function(){returnnewPromise(function(){})}
然后我們就可以這么使用了:
newPromise(function(resolve,reject){resolve(42)}).then(function(value){// "Big ERROR!!!"returnPromise.stop()}).catch().then().then().catch().then()
看起來是不是有語義的多?
關(guān)于停掉Promise鏈這個(gè)問題,我后面找到了更好的解決辦法,也整理出了另一篇文章,詳見這里:從如何停掉Promise鏈說起
Promise鏈上返回的最后一個(gè)Promise出錯(cuò)了怎么辦?
考慮如下代碼:
newPromise(function(resolve){resolve(42)}).then(function(value){alert(value)})
乍一看好像沒什么問題,但運(yùn)行這段代碼的話你會(huì)發(fā)現(xiàn)什么現(xiàn)象也不會(huì)發(fā)生,既不會(huì)alter出42,也不會(huì)在控制臺(tái)報(bào)錯(cuò),怎么回事呢。細(xì)看最后一行,alter被打成了alert,那為什么控制臺(tái)也沒有報(bào)錯(cuò)呢,因?yàn)閍lert所在的函數(shù)是被包在try/catch塊里的,alert這個(gè)變量找不到就直接拋錯(cuò)了,這個(gè)錯(cuò)就正好成了then返回的Promise的rejection reason。
也就是說,在Promise鏈的最后一個(gè)then里出現(xiàn)的錯(cuò)誤,非常難以發(fā)現(xiàn),有文章指出,可以在所有的Promise鏈的最后都加上一個(gè)catch,這樣出錯(cuò)后就能被捕獲到,這種方法確實(shí)是可行的,但是首先在每個(gè)地方都加上幾乎相同的代碼,違背了DRY原則,其次也相當(dāng)?shù)姆爆崱A硗猓詈笠粋€(gè)catch依然返回一個(gè)Promise,除非你能保證這個(gè)catch里的函數(shù)不再出錯(cuò),否則問題依然存在。在Q中有一個(gè)方法叫done,把這個(gè)方法鏈到Promise鏈的最后,它就能夠捕獲前面未處理的錯(cuò)誤,這其實(shí)跟在每個(gè)鏈后面加上catch沒有太大的區(qū)別,只是由框架來做了這件事,相當(dāng)于它提供了一個(gè)不會(huì)出錯(cuò)的catch鏈,我們可以這么實(shí)現(xiàn)done方法:
Promise.prototype.done=function(){returnthis.catch(function(e){// 此處一定要確保這個(gè)函數(shù)不能再出錯(cuò)console.error(e)})}
可是,能不能在不加catch或者done的情況下,也能夠讓開發(fā)者發(fā)現(xiàn)Promise鏈最后的錯(cuò)誤呢?答案依然是肯定的。
我們可以在一個(gè)Promise被reject的時(shí)候檢查這個(gè)Promise的onRejectedCallback數(shù)組,如果它為空,則說明它的錯(cuò)誤將沒有函數(shù)處理,這個(gè)時(shí)候,我們需要把錯(cuò)誤輸出到控制臺(tái),讓開發(fā)者可以發(fā)現(xiàn)。以下為具體實(shí)現(xiàn):
functionreject(reason){setTimeout(function(){if(self.status==='pending'){self.status='rejected'self.data=reasonif(self.onRejectedCallback.length===0){console.error(reason)}for(vari=0;i
上面的代碼對(duì)于以下的Promise鏈也能處理的很好:
newPromise(function(){// promise1reject(3)}).then()// returns promise2.then()// returns promise3.then()// returns promise4
看起來,promise1,2,3,4都沒有處理函數(shù),那是不是會(huì)在控制臺(tái)把這個(gè)錯(cuò)誤輸出4次呢,并不會(huì),實(shí)際上,promise1,2,3都隱式的有處理函數(shù),就是then的默認(rèn)參數(shù),各位應(yīng)該還記得then的默認(rèn)參數(shù)最終是被push到了Promise的callback數(shù)組里。只有promise4是真的沒有任何callback,因?yàn)閴焊蜎]有調(diào)用它的then方法。
事實(shí)上,Bluebird和ES6 Promise都做了類似的處理,在Promise被reject但又沒有callback時(shí),把錯(cuò)誤輸出到控制臺(tái)。
Q使用了done方法來達(dá)成類似的目的,$q在最新的版本中也加入了類似的功能。
Angular里的$q跟其它Promise的交互
一般來說,我們不會(huì)在Angular里使用其它的Promise,因?yàn)锳ngular已經(jīng)集成了$q,但有些時(shí)候我們?cè)贏ngular里需要用到其它的庫(比如LeanCloud的JS SDK),而這些庫或是封裝了ES6的Promise,或者是自己實(shí)現(xiàn)了Promise,這時(shí)如果你在Angular里使用這些庫,就有可能發(fā)現(xiàn)視圖跟Model不同步。究其原因,是因?yàn)?q已經(jīng)集成了Angular的digest loop機(jī)制,在Promise被resolve或reject時(shí)觸發(fā)digest,而其它的Promise顯然是不會(huì)集成的,所以如果你運(yùn)行下面這樣的代碼,視圖是不會(huì)同步的:
app.controller(function($scope){Promise.resolve(42).then(function(value){$scope.value=value})})
Promise結(jié)束時(shí)并不會(huì)觸發(fā)digest,所以視圖沒有同步。$q上正好有個(gè)when方法,它可以把其它的Promise轉(zhuǎn)換成$q的Promise(有些Promise實(shí)現(xiàn)中提供了Promise.cast函數(shù),用于將一個(gè)thenable轉(zhuǎn)換為它的Promise),問題就解決了:
app.controller(function($scope,$q){$q.when(Promise.resolve(42)).then(function(value){$scope.value=value})})
當(dāng)然也有其它的解決方案比如在其它Promise的鏈的最后加一個(gè)digest,類似下面這樣:
Promise.prototype.$digest=function(){$rootScope.$digest()returnthis}// 然后這么使用OtherPromise.resolve(42).then(function(value){$scope.value=value}).$digest()
因?yàn)槭褂脠鼍安⒉欢啵颂幉蛔錾钊胗懻摗?/p>
出錯(cuò)時(shí),是用throw new Error()還是用return Promise.reject(new Error())呢?
這里我覺得主要從性能和編碼的舒適度角度考慮:
性能方面,throw new Error()會(huì)使代碼進(jìn)入catch塊里的邏輯(還記得我們把所有的回調(diào)都包在try/catch里了吧),傳說throw用多了會(huì)影響性能,因?yàn)橐坏玹hrow,代碼就有可能跳到不可預(yù)知的位置。
但考慮到onResolved/onRejected函數(shù)是直接被包在Promise實(shí)現(xiàn)里的try里,出錯(cuò)后就直接進(jìn)入了這個(gè)try對(duì)應(yīng) 的catch塊,代碼的跳躍“幅度”相對(duì)較小,我認(rèn)為這里的性能損失可以忽略不記。有機(jī)會(huì)可以測試一下。
而使用Promise.reject(new Error()),則需要構(gòu)造一個(gè)新的Promise對(duì)象(里面包含2個(gè)數(shù)組,4個(gè)函數(shù):resolve/reject,onResolved/onRejected),也會(huì)花費(fèi)一定的時(shí)間和內(nèi)存。
而從編碼舒適度的角度考慮,出錯(cuò)用throw,正常時(shí)用return,可以比較明顯的區(qū)分出錯(cuò)與正常,throw和return又同為關(guān)鍵字,用來處理對(duì)應(yīng)的情況也顯得比較對(duì)稱(-_-)。另外在一般的編輯器里,Promise.reject不會(huì)被高亮成與throw和return一樣的顏色。最后,如果開發(fā)者又不喜歡構(gòu)造出一個(gè)Error對(duì)象的話,Error的高亮也沒有了。
綜上,我覺得在Promise里發(fā)現(xiàn)顯式的錯(cuò)誤后,用throw拋出錯(cuò)誤會(huì)比較好,而不是顯式的構(gòu)造一個(gè)被reject的Promise對(duì)象。
最佳實(shí)踐
這里不免再啰嗦兩句最佳實(shí)踐
一是不要把Promise寫成嵌套結(jié)構(gòu),至于怎么改進(jìn),這里就不多說了
// 錯(cuò)誤的寫法promise1.then(function(value){promise1.then(function(value){promise1.then(function(value){})})})
二是鏈?zhǔn)絇romise要返回一個(gè)Promise,而不只是構(gòu)造一個(gè)Promise
// 錯(cuò)誤的寫法Promise.resolve(1).then(function(){Promise.resolve(2)}).then(function(){Promise.resolve(3)})
Promise相關(guān)的convenience method的實(shí)現(xiàn)
請(qǐng)到這里查看Promise.race, Promise.all, Promise.resolve, Promise.reject等方法的具體實(shí)現(xiàn),這里就不具體解釋了,總的來說,只要then的實(shí)現(xiàn)是沒有問題的,其它所有的方法都可以非常方便的依賴then來實(shí)現(xiàn)。
結(jié)語
最后,如果你覺得這篇文章對(duì)你有所幫助,歡迎分享給你的朋友或者團(tuán)隊(duì),記得注明出處哦~