//本文內(nèi)容起初摘抄于 阮一峰 作者的譯文,用于記錄和學(xué)習(xí)蒙保,建議觀者移步于原文
概念:
? ? ?所謂的Promise严拒,簡(jiǎn)單來(lái)說(shuō)就是一個(gè)容器,里面保存著某個(gè)未來(lái)才會(huì)結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果奶甘。從語(yǔ)法上說(shuō),Promise是一個(gè)對(duì)象祭椰,從它可以獲取異步操作的消息甩十。
特點(diǎn):
? ? ? 1、Promise對(duì)象有三種狀態(tài):Pending(進(jìn)行中吭产,adj. 未決定的侣监;行將發(fā)生的)、Resolved(已完成臣淤,又稱為Fulfilled)和Rejected(已失敗)橄霉,只有異步操作的結(jié)果才能決定其所處的狀態(tài)。
? ? ? 2邑蒋、Promise的狀態(tài)一旦改變姓蜂,就不會(huì)再變,變化只有兩種:從Pending變?yōu)镽esolved或從Pending變?yōu)镽ejected医吊。狀態(tài)改變后就會(huì)凝固钱慢,意思為如果狀態(tài)已經(jīng)發(fā)生變化,再對(duì)Promise對(duì)象添加回調(diào)函數(shù)卿堂,也會(huì)立即得到結(jié)果束莫。這一點(diǎn)與事件(Event)完全不同,事件的特點(diǎn)是草描,如果你錯(cuò)過(guò)了它览绿,再去監(jiān)聽(tīng),是得不到結(jié)果的穗慕。
缺點(diǎn):Promise的缺點(diǎn)包括饿敲,無(wú)法取消Promise,一旦新建Promise則會(huì)立即執(zhí)行逛绵,無(wú)法中途取消怀各。其次倔韭,如果不設(shè)置回調(diào)函數(shù),Promise的內(nèi)部拋出的錯(cuò)誤就不會(huì)反應(yīng)到外部瓢对。第三寿酌,當(dāng)處于Pending狀態(tài)時(shí),無(wú)法得知目前具體進(jìn)展的階段沥曹。
如果某些事件不斷反復(fù)發(fā)生份名,一般來(lái)說(shuō)碟联,使用stream模式是比部署Promise更好的選擇妓美。
使用方法:
//Promise構(gòu)造函數(shù)接受一個(gè)函數(shù)作為參數(shù),該函數(shù)的兩個(gè)參數(shù)分別為resolve和reject鲤孵,他們是兩個(gè)函數(shù)壶栋,由Javascript引擎提供,不用自己部署普监;
var promise = new Promise(function(resolve,reject){
//...some code
if(/*異步操作成功*/){resolve(value);
}else{ ?reject(error);?};
});
//創(chuàng)建實(shí)例后通過(guò)promise.then方法添加回調(diào)函數(shù)
promise.then(function(value){
/*success*/},function(error){
/*failure*/});
示例:
異步加載圖片:
function loadImageAsync(url){
? ? ? return new Promise(function(resolve,reject){
? ? ? ? ? var image = new Image();
? ? ? ? ? image.onload = function(){resolve(image)};
? ? ? ? ? image.onerror = function(){reject(new Error("could not load image at"+url))};
? ? ? ? ? image.src = url;
? ? ? });
};
使用Promise實(shí)現(xiàn)Ajax操作,對(duì)XMLHttpRequest對(duì)象進(jìn)行封裝贵试,用于發(fā)出一個(gè)針對(duì)JSON數(shù)據(jù)的HTTP請(qǐng)求:
var getJson = function(url){
var promise = new Promise(function(resolve,reject){
var client = new XMLHttpRequest();client.open("GET",url);
client.onreadystatechange = handler;client.responseType = "json";
client.setRequestHeader("Accept","application/json");client.send();
function handler(){
if(this.readyState !==4){return};
if(this.status === 200){resolve(this.response);}else{reject(new Error(this.statusText))};
};});};
getJson("/posts.json").then(function(json){
console.log('contents:'+json)},function(error){console.error("Error",error)});
注意:resolve的參數(shù)除了正常值意外,還可能是另一個(gè)Promise實(shí)例凯正,如下:
var p1 = new Promise(function(resolve,reject){});
var p2 = new Promise(function(resolve,reject){/*code*/ resolve(p1)});
以上毙玻,p1的狀態(tài)會(huì)傳遞給p2,意指p1的狀態(tài)決定p2的狀態(tài)。如果p1的狀態(tài)是Pending,那么p2的回調(diào)函數(shù)會(huì)等待p1的狀態(tài)改變廊散;如果p1的狀態(tài)已經(jīng)是Resolved或者rejected桑滩,那么p2的回調(diào)函數(shù)將會(huì)立即執(zhí)行。
方法講解:
Promise.prototype.then();
Promise實(shí)例具有then方法允睹,也就是說(shuō)运准,then方法是定義在原型對(duì)象Promise.prototype上的。它的作用是為Promise實(shí)例添加狀態(tài)改變時(shí)的回調(diào)函數(shù)缭受。前面說(shuō)過(guò)胁澳,then方法的第一個(gè)參數(shù)是Resolved狀態(tài)的回調(diào)函數(shù),第二個(gè)參數(shù)(可選)是Rejected狀態(tài)的回調(diào)函數(shù)米者。
then方法返回的是一個(gè)新的Promise實(shí)例(注意韭畸,不是原來(lái)那個(gè)Promise實(shí)例)。因此可以采用鏈?zhǔn)綄?xiě)法蔓搞,即then方法后面再調(diào)用另一個(gè)then方法陆盘。
getJSON("/posts.json").then(function(json){returnjson.post;}).then(function(post){// ...});
上面的代碼使用then方法,依次指定了兩個(gè)回調(diào)函數(shù)败明。第一個(gè)回調(diào)函數(shù)完成以后隘马,會(huì)將返回結(jié)果作為參數(shù),傳入第二個(gè)回調(diào)函數(shù)妻顶。
采用鏈?zhǔn)降膖hen酸员,可以指定一組按照次序調(diào)用的回調(diào)函數(shù)蜒车。這時(shí),前一個(gè)回調(diào)函數(shù)幔嗦,有可能返回的還是一個(gè)Promise對(duì)象(即有異步操作)酿愧,這時(shí)后一個(gè)回調(diào)函數(shù),就會(huì)等待該P(yáng)romise對(duì)象的狀態(tài)發(fā)生變化邀泉,才會(huì)被調(diào)用嬉挡。
Promise.prototype.catch();
Promise.prototype.catch方法是.then(null, rejection)的別名,用于指定發(fā)生錯(cuò)誤時(shí)的回調(diào)函數(shù)汇恤。
getJSON("/posts.json").then(function(posts){// ...}).catch(function(error){// 處理 getJSON 和 前一個(gè)回調(diào)函數(shù)運(yùn)行時(shí)發(fā)生的錯(cuò)誤console.log('發(fā)生錯(cuò)誤庞钢!',error);});
上面代碼中,getJSON方法返回一個(gè)Promise對(duì)象因谎,如果該對(duì)象狀態(tài)變?yōu)镽esolved基括,則會(huì)調(diào)用then方法指定的回調(diào)函數(shù);如果異步操作拋出錯(cuò)誤财岔,狀態(tài)就會(huì)變?yōu)镽ejected风皿,就會(huì)調(diào)用catch方法指定的回調(diào)函數(shù),處理這個(gè)錯(cuò)誤匠璧。另外桐款,then方法指定的回調(diào)函數(shù),如果運(yùn)行中拋出錯(cuò)誤夷恍,也會(huì)被catch方法捕獲魔眨。
reject方法的作用,等同于拋出錯(cuò)誤裁厅,但如果Promise狀態(tài)已經(jīng)變成Resolved冰沙,再拋出錯(cuò)誤是無(wú)效的。
Promise對(duì)象的錯(cuò)誤具有“冒泡”性質(zhì)执虹,會(huì)一直向后傳遞拓挥,直到被捕獲為止。也就是說(shuō)袋励,錯(cuò)誤總是會(huì)被下一個(gè)catch語(yǔ)句捕獲侥啤。
getJSON("/post/1.json").then(function(post){returngetJSON(post.commentURL);}).then(function(comments){// some code}).catch(function(error){// 處理前面三個(gè)Promise產(chǎn)生的錯(cuò)誤});
上面代碼中,一共有三個(gè)Promise對(duì)象:一個(gè)由getJSON產(chǎn)生茬故,兩個(gè)由then產(chǎn)生盖灸。它們之中任何一個(gè)拋出的錯(cuò)誤,都會(huì)被最后一個(gè)catch捕獲磺芭。
一般來(lái)說(shuō)赁炎,不要在then方法里面定義Reject狀態(tài)的回調(diào)函數(shù)(即then的第二個(gè)參數(shù)),總是使用catch方法钾腺。
跟傳統(tǒng)的try/catch代碼塊不同的是徙垫,如果沒(méi)有使用catch方法指定錯(cuò)誤處理的回調(diào)函數(shù)讥裤,Promise對(duì)象拋出的錯(cuò)誤不會(huì)傳遞到外層代碼,即不會(huì)有任何反應(yīng)姻报。
Promise.resolve().catch(function(error){console.log('oh no',error);}).then(function(){console.log('carry on');});
需要注意的是己英,如上:catch方法返回的還是一個(gè)Promise對(duì)象,因此后面還可以接著調(diào)用then方法吴旋,如果沒(méi)有報(bào)錯(cuò)损肛,則會(huì)跳過(guò)catch方法。
Promise.all();
用于將多個(gè)Promise實(shí)例荣瑟,包裝成一個(gè)新的Promise實(shí)例:var p = Promise.all([p1治拿,p2,p3]);
上面代碼中褂傀,Promise.all方法接受一個(gè)數(shù)組作為參數(shù)忍啤,p1加勤、p2仙辟、p3都是Promise對(duì)象的實(shí)例,如果不是鳄梅,就會(huì)先調(diào)用下面講到的Promise.resolve方法叠国,將參數(shù)轉(zhuǎn)為Promise實(shí)例,再進(jìn)一步處理戴尸。(Promise.all方法的參數(shù)可以不是數(shù)組粟焊,但必須具有Iterator接口,且返回的每個(gè)成員都是Promise實(shí)例孙蒙。)
p的狀態(tài)由p1项棠、p2、p3決定挎峦,分成兩種情況香追。
(1)只有p1、p2坦胶、p3的狀態(tài)都變成fulfilled透典,p的狀態(tài)才會(huì)變成fulfilled,此時(shí)p1顿苇、p2峭咒、p3的返回值組成一個(gè)數(shù)組,傳遞給p的回調(diào)函數(shù)纪岁。
(2)只要p1凑队、p2、p3之中有一個(gè)被rejected幔翰,p的狀態(tài)就變成rejected漩氨,此時(shí)第一個(gè)被reject的實(shí)例的返回值短条,會(huì)傳遞給p的回調(diào)函數(shù)。
下面是一個(gè)具體的例子才菠。
// 生成一個(gè)Promise對(duì)象的數(shù)組varpromises=[2,3,5,7,11,13].map(function(id){returngetJSON("/post/"+id+".json");});Promise.all(promises).then(function(posts){// ...}).catch(function(reason){// ...});
上面代碼中茸时,promises是包含6個(gè)Promise實(shí)例的數(shù)組,只有這6個(gè)實(shí)例的狀態(tài)都變成fulfilled赋访,或者其中有一個(gè)變?yōu)閞ejected可都,才會(huì)調(diào)用Promise.all方法后面的回調(diào)函數(shù)。
Promise.race()
Promise.race方法同樣是將多個(gè)Promise實(shí)例蚓耽,包裝成一個(gè)新的Promise實(shí)例渠牲。
varp=Promise.race([p1,p2,p3]);
上面代碼中,只要p1步悠、p2签杈、p3之中有一個(gè)實(shí)例率先改變狀態(tài),p的狀態(tài)就跟著改變鼎兽。那個(gè)率先改變的Promise實(shí)例的返回值答姥,就傳遞給p的回調(diào)函數(shù)。
Promise.race方法的參數(shù)與Promise.all方法一樣谚咬,如果不是Promise實(shí)例鹦付,就會(huì)先調(diào)用下面講到的Promise.resolve方法,將參數(shù)轉(zhuǎn)為Promise實(shí)例择卦,再進(jìn)一步處理敲长。
下面是一個(gè)例子,如果指定時(shí)間內(nèi)沒(méi)有獲得結(jié)果秉继,就將Promise的狀態(tài)變?yōu)閞eject祈噪,否則變?yōu)閞esolve。
varp=Promise.race([fetch('/resource-that-may-take-a-while'),newPromise(function(resolve,reject){setTimeout(()=>reject(newError('request timeout')),5000)})])p.then(response=>console.log(response))p.catch(error=>console.log(error))
上面代碼中尚辑,如果5秒之內(nèi)fetch方法無(wú)法返回結(jié)果辑鲤,變量p的狀態(tài)就會(huì)變?yōu)閞ejected,從而觸發(fā)catch方法指定的回調(diào)函數(shù)腌巾。
Promise.resolve
有時(shí)需要將現(xiàn)有對(duì)象轉(zhuǎn)為Promise對(duì)象遂填,Promise.resolve方法就起到這個(gè)作用。
varjsPromise=Promise.resolve($.ajax('/whatever.json'));
上面代碼將jQuery生成的deferred對(duì)象澈蝙,轉(zhuǎn)為一個(gè)新的Promise對(duì)象吓坚。
Promise.resolve等價(jià)于下面的寫(xiě)法。
Promise.resolve('foo')// 等價(jià)于newPromise(resolve=>resolve('foo'))
Promise.resolve方法的參數(shù)分成四種情況灯荧。
(1)參數(shù)是一個(gè)Promise實(shí)例
如果參數(shù)是Promise實(shí)例礁击,那么Promise.resolve將不做任何修改、原封不動(dòng)地返回這個(gè)實(shí)例。
(2)參數(shù)是一個(gè)thenable對(duì)象
thenable對(duì)象指的是具有then方法的對(duì)象哆窿,比如下面這個(gè)對(duì)象链烈。
letthenable={then:function(resolve,reject){resolve(42);}};
Promise.resolve方法會(huì)將這個(gè)對(duì)象轉(zhuǎn)為Promise對(duì)象,然后就立即執(zhí)行thenable對(duì)象的then方法挚躯。
letthenable={then:function(resolve,reject){resolve(42);}};letp1=Promise.resolve(thenable);p1.then(function(value){console.log(value);// 42});
上面代碼中强衡,thenable對(duì)象的then方法執(zhí)行后,對(duì)象p1的狀態(tài)就變?yōu)閞esolved码荔,從而立即執(zhí)行最后那個(gè)then方法指定的回調(diào)函數(shù)漩勤,輸出42。
(3)參數(shù)不是具有then方法的對(duì)象缩搅,或根本就不是對(duì)象
如果參數(shù)是一個(gè)原始值越败,或者是一個(gè)不具有then方法的對(duì)象,則Promise.resolve方法返回一個(gè)新的Promise對(duì)象硼瓣,狀態(tài)為Resolved究飞。
varp=Promise.resolve('Hello');p.then(function(s){console.log(s)});// Hello
上面代碼生成一個(gè)新的Promise對(duì)象的實(shí)例p。由于字符串Hello不屬于異步操作(判斷方法是它不是具有then方法的對(duì)象)堂鲤,返回Promise實(shí)例的狀態(tài)從一生成就是Resolved亿傅,所以回調(diào)函數(shù)會(huì)立即執(zhí)行。Promise.resolve方法的參數(shù)筑累,會(huì)同時(shí)傳給回調(diào)函數(shù)袱蜡。
(4)不帶有任何參數(shù)
Promise.resolve方法允許調(diào)用時(shí)不帶參數(shù)丝蹭,直接返回一個(gè)Resolved狀態(tài)的Promise對(duì)象慢宗。
所以,如果希望得到一個(gè)Promise對(duì)象奔穿,比較方便的方法就是直接調(diào)用Promise.resolve方法镜沽。
varp=Promise.resolve();p.then(function(){// ...});
上面代碼的變量p就是一個(gè)Promise對(duì)象。
需要注意的是贱田,立即resolve的Promise對(duì)象缅茉,是在本輪“事件循環(huán)”(event loop)的結(jié)束時(shí),而不是在下一輪“事件循環(huán)”的開(kāi)始時(shí)男摧。
setTimeout(function(){console.log('three');},0);Promise.resolve().then(function(){console.log('two');});console.log('one');// one// two// three
上面代碼中蔬墩,setTimeout(fn, 0)在下一輪“事件循環(huán)”開(kāi)始時(shí)執(zhí)行,Promise.resolve()在本輪“事件循環(huán)”結(jié)束時(shí)執(zhí)行耗拓,console.log(’one‘)則是立即執(zhí)行拇颅,因此最先輸出。
Promise.reject()
Promise.reject(reason)方法也會(huì)返回一個(gè)新的Promise實(shí)例乔询,該實(shí)例的狀態(tài)為rejected樟插。它的參數(shù)用法與Promise.resolve方法完全一致。
varp=Promise.reject('出錯(cuò)了');// 等同于varp=newPromise((resolve,reject)=>reject('出錯(cuò)了'))p.then(null,function(s){console.log(s)});// 出錯(cuò)了
上面代碼生成一個(gè)Promise對(duì)象的實(shí)例p,狀態(tài)為rejected黄锤,回調(diào)函數(shù)會(huì)立即執(zhí)行搪缨。
另兩個(gè)附加方法:
done()
Promise對(duì)象的回調(diào)鏈,不管以then方法或catch方法結(jié)尾鸵熟,要是最后一個(gè)方法拋出錯(cuò)誤副编,都有可能無(wú)法捕捉到(因?yàn)镻romise內(nèi)部的錯(cuò)誤不會(huì)冒泡到全局)。因此流强,我們可以提供一個(gè)done方法齿桃,總是處于回調(diào)鏈的尾端,保證拋出任何可能出現(xiàn)的錯(cuò)誤煮盼。
asyncFunc().then(f1).catch(r1).then(f2).done();
它的實(shí)現(xiàn)代碼相當(dāng)簡(jiǎn)單短纵。
Promise.prototype.done=function(onFulfilled,onRejected){this.then(onFulfilled,onRejected).catch(function(reason){// 拋出一個(gè)全局錯(cuò)誤setTimeout(()=>{throwreason},0);});};
從上面代碼可見(jiàn),done方法的使用僵控,可以像then方法那樣用香到,提供Fulfilled和Rejected狀態(tài)的回調(diào)函數(shù),也可以不提供任何參數(shù)报破。但不管怎樣悠就,done都會(huì)捕捉到任何可能出現(xiàn)的錯(cuò)誤,并向全局拋出充易。
finally();
finally方法用于指定不管Promise對(duì)象最后狀態(tài)如何梗脾,都會(huì)執(zhí)行的操作。它與done方法的最大區(qū)別盹靴,它接受一個(gè)普通的回調(diào)函數(shù)作為參數(shù)炸茧,該函數(shù)不管怎樣都必須執(zhí)行。
下面是一個(gè)例子稿静,服務(wù)器使用Promise處理請(qǐng)求梭冠,然后使用finally方法關(guān)掉服務(wù)器。
server.listen(0).then(function(){// run test}).finally(server.stop);
它的實(shí)現(xiàn)也很簡(jiǎn)單改备。
Promise.prototype.finally=function(callback){letP=this.constructor;returnthis.then(value=>P.resolve(callback()).then(()=>value),reason=>P.resolve(callback()).then(()=>{throwreason}));};
上面代碼中控漠,不管前面的Promise是fulfilled還是rejected,都會(huì)執(zhí)行回調(diào)函數(shù)callback悬钳。
應(yīng)用
加載圖片
我們可以將圖片的加載寫(xiě)成一個(gè)Promise盐捷,一旦加載完成,Promise的狀態(tài)就發(fā)生變化默勾。
const preloadImage=function(path){returnnewPromise(function(resolve,reject){varimage=newImage();image.onload=resolve;image.onerror=reject;image.src=path;});};
Generator函數(shù)與Promise的結(jié)合
使用Generator函數(shù)管理流程碉渡,遇到異步操作的時(shí)候,通常返回一個(gè)Promise對(duì)象灾测。
functiongetFoo(){returnnewPromise(function(resolve,reject){resolve('foo');});}varg=function*(){try{varfoo=yieldgetFoo();console.log(foo);}catch(e){console.log(e);}};functionrun(generator){varit=generator();functiongo(result){if(result.done)returnresult.value;returnresult.value.then(function(value){returngo(it.next(value));},function(error){returngo(it.throw(error));});}go(it.next());}run(g);
上面代碼的Generator函數(shù)g之中爆价,有一個(gè)異步操作getFoo垦巴,它返回的就是一個(gè)Promise對(duì)象。函數(shù)run用來(lái)處理這個(gè)Promise對(duì)象铭段,并調(diào)用下一個(gè)next方法骤宣。