一、前言
以往的經(jīng)驗告訴我改橘,在接觸自己比較陌生的名詞和技術(shù)前首先要問三個問題:
- 它是用來做什么的滋尉?
- 它是如何實(shí)現(xiàn)的?
- 沒有它飞主,應(yīng)該怎么辦狮惜?
今天我們主要關(guān)注第一個問題點(diǎn),淺析一下Promise的使用場景和一些特點(diǎn)碌识。
二碾篡、定義
A
Promise
is an object representing the eventual completion or failure of an asynchronous operation.
這是MDN上對Promise的描述:Promise 是一個對象,它表現(xiàn)了一個異步操作最終的完成狀態(tài)或者失敗狀態(tài)筏餐。
簡單點(diǎn)說开泽,Promise是為了更好地寫異步操作而產(chǎn)生的,它保存著一個未來才會結(jié)束的事件的結(jié)果魁瞪。
三穆律、特點(diǎn)
- 對象狀態(tài)不受外界影響,Promise對象有三種狀態(tài)佩番,pending(進(jìn)行中)众旗、fulfilled(成功)和rejected(失敯丈肌)趟畏。只有異步操作的結(jié)果,可以決定當(dāng)前是哪一種狀態(tài)滩租,任何其他操作都無法改變這個狀態(tài)赋秀。這也是Promise這個名字的由來,它的英語意思就是“承諾”律想,表示其他手段無法改變猎莲。
2.一旦狀態(tài)改變,就不會再變技即,任何時候都可以得到這個結(jié)果著洼。Promise對象的狀態(tài)改變,只有兩種可能:從pending變?yōu)?strong>fulfilled和從pending變?yōu)?strong>rejected。只要這兩種情況發(fā)生身笤,狀態(tài)就凝固了豹悬,不會再變了,會一直保持這個結(jié)果瞻佛,這時就稱為 resolved(已完成)或者 settled。如果改變已經(jīng)發(fā)生了娇钱,你再對Promise對象添加回調(diào)函數(shù),也會立即得到這個結(jié)果文搂。這與事件(Event)完全不同,事件的特點(diǎn)是细疚,如果你錯過了它蔗彤,再去監(jiān)聽,是得不到結(jié)果的疯兼。
tips:很多教程里把resolved(已完成)等價于fullfilled(成功)狀態(tài),下文中Promise狀態(tài)定型為resolved包含fullfilled和rejected兩種狀態(tài)然遏。但是由于習(xí)慣寫法,Promise中“成功”的回調(diào)函數(shù)的名字依然叫做resolve待侵。
四、使用
在使用Promise之前我們先看看我們在沒有Promise時是如何寫異步操作的姨裸,以ajax請求為例子:
eg1:
$.ajax('url1')
.done((res)=>{
$.ajax('url2')
.done((res)=>{
$.ajax('url3')
.done((res)=>{
console.log(res)
})
})
})
這種有“層次感”的代碼即callback hell,一旦嵌套三層以上就十分影響可讀性傀缩,也容易出錯那先。
再看看Promise如何實(shí)現(xiàn)上面的需求:
eg2:
Promise.resolve($.ajax('url1'))
.then((res)=>$.ajax('url2'))
.then((res)=>$.ajax('url3'))
.then((res)=>console.log(res))
如上,我們可以看到代碼不再是層層嵌套赡艰,這樣寫異步操作更為直觀售淡。
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
說明:
- Promise是一個構(gòu)造函數(shù),使用new可以生產(chǎn)Promise實(shí)例;
- 構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù)慷垮,該函數(shù)的兩個參數(shù)分別是resolve和reject。它們是兩個函數(shù)料身,由 JavaScript 引擎提供,不用自己部署芹血。
- resolve函數(shù)的作用是楞慈,將Promise對象的狀態(tài)從“未完成”變?yōu)椤俺晒Α保磸?pending 變?yōu)?fullfilled),在異步操作成功時調(diào)用啃擦,并將異步操作的結(jié)果,作為參數(shù)傳遞出去议惰;reject函數(shù)的作用是,將Promise對象的狀態(tài)從“未完成”變?yōu)椤笆 保磸?pending 變?yōu)?rejected)言询,在異步操作失敗時調(diào)用,并將異步操作報出的錯誤运杭,作為參數(shù)傳遞出去。
- Promise實(shí)例生成以后辆憔,可以用then方法分別指定fullfilled狀態(tài)和rejected狀態(tài)的回調(diào)函數(shù)撇眯。(then的參數(shù)是兩個回調(diào)函數(shù))
2. 使用Promise.prototype.catch()捕獲錯誤
Promise有3個缺點(diǎn):
- 無法取消Promise虱咧,一旦新建它就會立即執(zhí)行,無法中途取消腕巡。
- 如果不設(shè)置回調(diào)函數(shù),Promise內(nèi)部拋出的錯誤绘沉,不會反應(yīng)到外部。
- 當(dāng)處于pending狀態(tài)時车伞,無法得知目前進(jìn)展到哪一個階段(剛剛開始還是即將完成)择懂。
現(xiàn)在我們針對第二點(diǎn)展開Promise實(shí)例的catch方法的使用
eg4:
makePromise()
.then(res=>$.ajax1(res))
.then(res=>$.ajax2(res))
.catch(err=>console.log(err)) //可以捕獲前面所有Promise對象的error
說明:
- catch一般在Promise鏈的最后一步調(diào)用另玖,它可以捕獲前面任何一個Promise對象的error;
- 雖然then的第二個參數(shù)可以獲取上一個Promise的error日矫,但是不提倡這么寫绑榴;應(yīng)為catch可以捕獲上面多個Promise的error哪轿,寫法也更容易理解翔怎;
- Promsie實(shí)例執(zhí)行完catch方法后窃诉,也會變成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é)會使用Promise.all()
Promise.all方法用于將多個 Promise 實(shí)例杨耙,包裝成一個新的 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)才會變成fulfilled,此時promise1羊苟、promise2塑陵、promise3的返回值組成一個數(shù)組蜡励,傳遞給newPromise的回調(diào)函數(shù)。
- 只要promise1凉倚、promise2、promise3之中有一個被rejected稽寒,p的狀態(tài)就變成rejected,此時第一個被reject的實(shí)例的返回值瓦胎,會傳遞給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中的三個Promise實(shí)例的狀態(tài)定型(resolved搔啊,包含fullfilled和rejected兩種狀態(tài))才會進(jìn)入進(jìn)入Promise.all后面的回調(diào):
- 如果3個Promise實(shí)例的定型狀態(tài)為fullfilled那么,Promise.all()得到的實(shí)例狀態(tài)也會是fullfilled负芋,3個Promise實(shí)例的返回值會放在一個數(shù)組中,作為入?yún)鹘oPromise.all后面then的第一個回調(diào)函數(shù)旧蛾;
- 如果3個Promise實(shí)例定型后,有任何一個實(shí)例的狀態(tài)為rejected锨天,那么Promise.all()得到的實(shí)例狀態(tài)為rejected,第一個為rejected的Promise的返回值會傳給Promise.all后面then的第二個回調(diào)函數(shù)病袄;
有一點(diǎn)值得注意的是:
如果作為Promise.all參數(shù)的 Promise 實(shí)例赘阀,如果自己定義了catch方法,那么它一旦被rejected脑奠,并不會觸發(fā)Promise.all()后面的catch方法。
原因在三.2中的說明里提到了:“Promise實(shí)例調(diào)用catch方法后宋欺,狀態(tài)會變?yōu)閒ullfilled”。也就是說在沒調(diào)用catch方法狀態(tài)為rejected的Promise實(shí)例齿诞,調(diào)用catch后狀態(tài)變?yōu)榱薴ullfilled,Promise.all()的狀態(tài)會變成fullfilled掌挚,當(dāng)然不會觸發(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é)果是一個數(shù)組吠式,代表newPromise的完成后的狀態(tài)為fullfilled,兩個Promise實(shí)例的返回值作為入?yún)鹘o了then,而newPromise后面的catch并不會捕獲到promise2中的error,應(yīng)為promise2自己catch了error特占。
學(xué)了這么多理論,下面說一下Promise.all的使用場景:
一個網(wǎng)頁是目,需要加載三個數(shù)據(jù)源的內(nèi)容(圖片、文字懊纳、背景音樂...),它們之間是沒有依賴關(guān)系的嗤疯,加載完成后取消loading:
//請求圖片的Promsie
let promisePic = new Promise((resolve,reject)=>{
$.ajax('url1').done(res=>resolve(res))
.fail(err=>reject(err))
})
//請求文字的Promise
let promiseText = new Promise((resolve,reject)=>{
$.ajax('url2').done(res=>resolve(res))
.fail(err=>reject(err))
})
//請求背景音樂的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 對象最后狀態(tài)如何,都會執(zhí)行的操作茂缚。該方法是 ES2018 引入標(biāo)準(zhǔn)的。
Promise.race([p1, p2, ...]):
和Promise.all類似脚囊,Promise.race方法同樣是接收多個 Promise 實(shí)例,但它返回最先fullfill的Promise的結(jié)果悔耘,如果有一個reject,就提前reject。
Promise.resolve(value):
有時需要將現(xiàn)有對象轉(zhuǎn)為 Promise 對象催首,Promise.resolve方法就起到這個作用泄鹏。
Promsie.reject(value):
Promise.reject(reason)方法也會返回一個新的 Promise 實(shí)例,該實(shí)例的狀態(tài)為rejected备籽。
5.新的提案(proposal)[2019.8.28更新]
Promise.allSettled([p1, p2, ...]):
和Promise.all()類似,接受由Promise對象組成的數(shù)組车猬,Promise.all()會在所有的Promise狀態(tài)為fullfilled或者其中一個狀態(tài)為rejected時返回,但是它不會管數(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()類型伏嗜,接受一個由Promise對象組成的數(shù)組,當(dāng)其中任何一個狀態(tài)變?yōu)閒ullfilled時承绸,就會返回這Promise的結(jié)果。但是和Promise.race()不同的是军熏,它在有Promise狀態(tài)為rejected時也不會立即返回轩猩,只有在所有Promise reject時才返回rejected 的Promise荡澎。
五、總結(jié)
1.介紹了Promise的作用摩幔、定義和特點(diǎn)浊猾;
2.列舉了Promise的簡單使用和錯誤捕獲方法热鞍;
3.簡單列舉了Promise部分方法的使用場景;
文中可能有不大嚴(yán)謹(jǐn)?shù)牡胤睫背瑁瑲g迎大家指出偷办。
關(guān)于Promise的更多講解可以參考
- 阮一峰老師的ECMAScript 6 入門
- 手寫一個Promise