一、前言
以往的經(jīng)驗(yàn)告訴我,在接觸自己比較陌生的名詞和技術(shù)前首先要問(wèn)三個(gè)問(wèn)題:
- 它是用來(lái)做什么的?
- 它是如何實(shí)現(xiàn)的?
- 沒(méi)有它,應(yīng)該怎么辦?
今天我們主要關(guān)注第一個(gè)問(wèn)題點(diǎn),淺析一下Promise的使用場(chǎng)景和一些特點(diǎn)。
二、定義
A
Promiseis an object representing the eventual completion or failure of an asynchronous operation.
這是MDN上對(duì)Promise的描述:Promise 是一個(gè)對(duì)象,它表現(xiàn)了一個(gè)異步操作最終的完成狀態(tài)或者失敗狀態(tài)。
簡(jiǎn)單點(diǎn)說(shuō),Promise是為了更好地寫(xiě)異步操作而產(chǎn)生的,它保存著一個(gè)未來(lái)才會(huì)結(jié)束的事件的結(jié)果。
三、特點(diǎn)
- 對(duì)象狀態(tài)不受外界影響,Promise對(duì)象有三種狀態(tài),pending(進(jìn)行中)、fulfilled(成功)和rejected(失?。V挥挟惒讲僮鞯慕Y(jié)果,可以決定當(dāng)前是哪一種狀態(tài),任何其他操作都無(wú)法改變這個(gè)狀態(tài)。這也是Promise這個(gè)名字的由來(lái),它的英語(yǔ)意思就是“承諾”,表示其他手段無(wú)法改變。
2.一旦狀態(tài)改變,就不會(huì)再變,任何時(shí)候都可以得到這個(gè)結(jié)果。Promise對(duì)象的狀態(tài)改變,只有兩種可能:從pending變?yōu)?strong>fulfilled和從pending變?yōu)?strong>rejected。只要這兩種情況發(fā)生,狀態(tài)就凝固了,不會(huì)再變了,會(huì)一直保持這個(gè)結(jié)果,這時(shí)就稱為 resolved(已完成)或者 settled。如果改變已經(jīng)發(fā)生了,你再對(duì)Promise對(duì)象添加回調(diào)函數(shù),也會(huì)立即得到這個(gè)結(jié)果。這與事件(Event)完全不同,事件的特點(diǎn)是,如果你錯(cuò)過(guò)了它,再去監(jiān)聽(tīng),是得不到結(jié)果的。
tips:很多教程里把resolved(已完成)等價(jià)于fullfilled(成功)狀態(tài),下文中Promise狀態(tài)定型為resolved包含fullfilled和rejected兩種狀態(tài)。但是由于習(xí)慣寫(xiě)法,Promise中“成功”的回調(diào)函數(shù)的名字依然叫做resolve。
四、使用
在使用Promise之前我們先看看我們?cè)跊](méi)有Promise時(shí)是如何寫(xiě)異步操作的,以ajax請(qǐng)求為例子:
eg1:
$.ajax('url1')
.done((res)=>{
$.ajax('url2')
.done((res)=>{
$.ajax('url3')
.done((res)=>{
console.log(res)
})
})
})
這種有“層次感”的代碼即callback hell,一旦嵌套三層以上就十分影響可讀性,也容易出錯(cuò)。
再看看Promise如何實(shí)現(xiàn)上面的需求:
eg2:
Promise.resolve($.ajax('url1'))
.then((res)=>$.ajax('url2'))
.then((res)=>$.ajax('url3'))
.then((res)=>console.log(res))
如上,我們可以看到代碼不再是層層嵌套,這樣寫(xiě)異步操作更為直觀。
1.基本用法
eg3:(先上代碼)
const makePromise = ()=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
let num = Math.random()*10
if(num >5){
resolve('resolve--->' + num)
}else{
reject('reject--->' + num)
}
})
},2000)
}
makePromise()
.then(str=>console.log(str),err=>console.log('oh no' + err)) //resolve--->8.866619911022287
說(shuō)明:
- Promise是一個(gè)構(gòu)造函數(shù),使用new可以生產(chǎn)Promise實(shí)例;
- 構(gòu)造函數(shù)接受一個(gè)函數(shù)作為參數(shù),該函數(shù)的兩個(gè)參數(shù)分別是resolve和reject。它們是兩個(gè)函數(shù),由 JavaScript 引擎提供,不用自己部署。
- resolve函數(shù)的作用是,將Promise對(duì)象的狀態(tài)從“未完成”變?yōu)椤俺晒Α保磸?pending 變?yōu)?fullfilled),在異步操作成功時(shí)調(diào)用,并將異步操作的結(jié)果,作為參數(shù)傳遞出去;reject函數(shù)的作用是,將Promise對(duì)象的狀態(tài)從“未完成”變?yōu)椤笆 保磸?pending 變?yōu)?rejected),在異步操作失敗時(shí)調(diào)用,并將異步操作報(bào)出的錯(cuò)誤,作為參數(shù)傳遞出去。
- Promise實(shí)例生成以后,可以用then方法分別指定fullfilled狀態(tài)和rejected狀態(tài)的回調(diào)函數(shù)。(then的參數(shù)是兩個(gè)回調(diào)函數(shù))
2. 使用Promise.prototype.catch()捕獲錯(cuò)誤
Promise有3個(gè)缺點(diǎn):
- 無(wú)法取消Promise,一旦新建它就會(huì)立即執(zhí)行,無(wú)法中途取消。
- 如果不設(shè)置回調(diào)函數(shù),Promise內(nèi)部拋出的錯(cuò)誤,不會(huì)反應(yīng)到外部。
- 當(dāng)處于pending狀態(tài)時(shí),無(wú)法得知目前進(jìn)展到哪一個(gè)階段(剛剛開(kāi)始還是即將完成)。
現(xiàn)在我們針對(duì)第二點(diǎn)展開(kāi)Promise實(shí)例的catch方法的使用
eg4:
makePromise()
.then(res=>$.ajax1(res))
.then(res=>$.ajax2(res))
.catch(err=>console.log(err)) //可以捕獲前面所有Promise對(duì)象的error
說(shuō)明:
- catch一般在Promise鏈的最后一步調(diào)用,它可以捕獲前面任何一個(gè)Promise對(duì)象的error;
- 雖然then的第二個(gè)參數(shù)可以獲取上一個(gè)Promise的error,但是不提倡這么寫(xiě);應(yīng)為catch可以捕獲上面多個(gè)Promise的error,寫(xiě)法也更容易理解;
- Promsie實(shí)例執(zhí)行完catch方法后,也會(huì)變成fullfilled;
eg5:
//bad
makePromise()
.then(res=>console.log(res),err=>console.log(err))
//good
makePromise()
.then(res=>console.log(res))
.catch(err=>console.log(err))
3.學(xué)會(huì)使用Promise.all()
Promise.all方法用于將多個(gè) Promise 實(shí)例,包裝成一個(gè)新的 Promise 實(shí)例。
eg6:
const newPromise = Promise.all([promise1,promise2,promise3])
newPromise的狀態(tài)由promise1,promise2,promise3共同決定;
分以下兩種情況:
- 只有promise1、promise2、promise3的狀態(tài)都變成fulfilled(完成),newPromise的狀態(tài)才會(huì)變成fulfilled,此時(shí)promise1、promise2、promise3的返回值組成一個(gè)數(shù)組,傳遞給newPromise的回調(diào)函數(shù)。
- 只要promise1、promise2、promise3之中有一個(gè)被rejected,p的狀態(tài)就變成rejected,此時(shí)第一個(gè)被reject的實(shí)例的返回值,會(huì)傳遞給p的回調(diào)函數(shù)。
eg7:
let arr = [1,2,3]
let promiseArr = arr.map((item=>makePromise(item))
Promise.all(promsieArr)
.then(resArr=>console.log(resArr.length)) //3
上面的例7中只有當(dāng)promiseArr中的三個(gè)Promise實(shí)例的狀態(tài)定型(resolved,包含fullfilled和rejected兩種狀態(tài))才會(huì)進(jìn)入進(jìn)入Promise.all后面的回調(diào):
- 如果3個(gè)Promise實(shí)例的定型狀態(tài)為fullfilled那么,Promise.all()得到的實(shí)例狀態(tài)也會(huì)是fullfilled,3個(gè)Promise實(shí)例的返回值會(huì)放在一個(gè)數(shù)組中,作為入?yún)鹘oPromise.all后面then的第一個(gè)回調(diào)函數(shù);
- 如果3個(gè)Promise實(shí)例定型后,有任何一個(gè)實(shí)例的狀態(tài)為rejected,那么Promise.all()得到的實(shí)例狀態(tài)為rejected,第一個(gè)為rejected的Promise的返回值會(huì)傳給Promise.all后面then的第二個(gè)回調(diào)函數(shù);
有一點(diǎn)值得注意的是:
如果作為Promise.all參數(shù)的 Promise 實(shí)例,如果自己定義了catch方法,那么它一旦被rejected,并不會(huì)觸發(fā)Promise.all()后面的catch方法。
原因在三.2中的說(shuō)明里提到了:“Promise實(shí)例調(diào)用catch方法后,狀態(tài)會(huì)變?yōu)閒ullfilled”。也就是說(shuō)在沒(méi)調(diào)用catch方法狀態(tài)為rejected的Promise實(shí)例,調(diào)用catch后狀態(tài)變?yōu)榱薴ullfilled,Promise.all()的狀態(tài)會(huì)變成fullfilled,當(dāng)然不會(huì)觸發(fā)Promise.all()后面的catch了。
eg8:
let promise1 = new Promise((resolve,reject)=>{
resolve('hello')
}).then(res=>res)
.catch(err=>err)
let promise2 = new Promise((resolve,reject)=>{
throw new Error('this is an error')
}).then(res=>res)
.catch(err=>err)
let newPromise = Promise.all([promise1,promise2])
newPromise.then(res=>console.log(res))
.catch(err=>console.log(err))
/*
["hello", Error: this is an error
at Promise (<anonymous>:7:12)
at new Promise (<anonymous>)
at <a…]
*/
可以看到打印的結(jié)果是一個(gè)數(shù)組,代表newPromise的完成后的狀態(tài)為fullfilled,兩個(gè)Promise實(shí)例的返回值作為入?yún)鹘o了then,而newPromise后面的catch并不會(huì)捕獲到promise2中的error,應(yīng)為promise2自己catch了error。
學(xué)了這么多理論,下面說(shuō)一下Promise.all的使用場(chǎng)景:
一個(gè)網(wǎng)頁(yè),需要加載三個(gè)數(shù)據(jù)源的內(nèi)容(圖片、文字、背景音樂(lè)...),它們之間是沒(méi)有依賴關(guān)系的,加載完成后取消loading:
//請(qǐng)求圖片的Promsie
let promisePic = new Promise((resolve,reject)=>{
$.ajax('url1').done(res=>resolve(res))
.fail(err=>reject(err))
})
//請(qǐng)求文字的Promise
let promiseText = new Promise((resolve,reject)=>{
$.ajax('url2').done(res=>resolve(res))
.fail(err=>reject(err))
})
//請(qǐng)求背景音樂(lè)的Promise
let promiseBgm = new Promise((resolve,reject)=>{
$.ajax('url3').done(res=>resolve(res))
.fail(err=>reject(err))
})
let promiseLoad = Promise.all([promsiePic,promiseText,promiseBgm])
promiseLoad.then(()=>{clearLoading()})
.catch((err)=>console.log(err))
4.更多方法
Promise.prototype.finally(fn):
finally方法用于指定不管 Promise 對(duì)象最后狀態(tài)如何,都會(huì)執(zhí)行的操作。該方法是 ES2018 引入標(biāo)準(zhǔn)的。
Promise.race([p1, p2, ...]):
和Promise.all類似,Promise.race方法同樣是接收多個(gè) Promise 實(shí)例,但它返回最先f(wàn)ullfill的Promise的結(jié)果,如果有一個(gè)reject,就提前reject。
Promise.resolve(value):
有時(shí)需要將現(xiàn)有對(duì)象轉(zhuǎn)為 Promise 對(duì)象,Promise.resolve方法就起到這個(gè)作用。
Promsie.reject(value):
Promise.reject(reason)方法也會(huì)返回一個(gè)新的 Promise 實(shí)例,該實(shí)例的狀態(tài)為rejected。
5.新的提案(proposal)[2019.8.28更新]
Promise.allSettled([p1, p2, ...]):
和Promise.all()類似,接受由Promise對(duì)象組成的數(shù)組,Promise.all()會(huì)在所有的Promise狀態(tài)為fullfilled或者其中一個(gè)狀態(tài)為rejected時(shí)返回,但是它不會(huì)管數(shù)組中Promise的狀態(tài),只要所有的Promise 狀態(tài)為settled(fullfilled or rejected)。
tips: This is useful in cases where you don’t care about the state of the promise, you just want to know when the work is done, regardless of whether it was successful.
Promise.any([p1, p2, ...]):
Promsie.any()和Promise.race()類型,接受一個(gè)由Promise對(duì)象組成的數(shù)組,當(dāng)其中任何一個(gè)狀態(tài)變?yōu)閒ullfilled時(shí),就會(huì)返回這Promise的結(jié)果。但是和Promise.race()不同的是,它在有Promise狀態(tài)為rejected時(shí)也不會(huì)立即返回,只有在所有Promise reject時(shí)才返回rejected 的Promise。
五、總結(jié)
1.介紹了Promise的作用、定義和特點(diǎn);
2.列舉了Promise的簡(jiǎn)單使用和錯(cuò)誤捕獲方法;
3.簡(jiǎn)單列舉了Promise部分方法的使用場(chǎng)景;
文中可能有不大嚴(yán)謹(jǐn)?shù)牡胤剑瑲g迎大家指出。
關(guān)于Promise的更多講解可以參考