//本文內(nèi)容起初摘抄于 阮一峰 作者的譯文,用于記錄和學(xué)習(xí),建議觀者移步于原文
概念:
? ? ?所謂的Promise,簡單來說就是一個容器,里面保存著某個未來才會結(jié)束的事件(通常是一個異步操作)的結(jié)果。從語法上說,Promise是一個對象,從它可以獲取異步操作的消息。
特點:
? ? ? 1、Promise對象有三種狀態(tài):Pending(進(jìn)行中,adj. 未決定的;行將發(fā)生的)、Resolved(已完成,又稱為Fulfilled)和Rejected(已失敗),只有異步操作的結(jié)果才能決定其所處的狀態(tài)。
? ? ? 2、Promise的狀態(tài)一旦改變,就不會再變,變化只有兩種:從Pending變?yōu)镽esolved或從Pending變?yōu)镽ejected。狀態(tài)改變后就會凝固,意思為如果狀態(tài)已經(jīng)發(fā)生變化,再對Promise對象添加回調(diào)函數(shù),也會立即得到結(jié)果。這一點與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監(jiān)聽,是得不到結(jié)果的。
缺點:Promise的缺點包括,無法取消Promise,一旦新建Promise則會立即執(zhí)行,無法中途取消。其次,如果不設(shè)置回調(diào)函數(shù),Promise的內(nèi)部拋出的錯誤就不會反應(yīng)到外部。第三,當(dāng)處于Pending狀態(tài)時,無法得知目前具體進(jìn)展的階段。
如果某些事件不斷反復(fù)發(fā)生,一般來說,使用stream模式是比部署Promise更好的選擇。
使用方法:
//Promise構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù)的兩個參數(shù)分別為resolve和reject,他們是兩個函數(shù),由Javascript引擎提供,不用自己部署;
var promise = new Promise(function(resolve,reject){
//...some code
if(/*異步操作成功*/){resolve(value);
}else{ ?reject(error);?};
});
//創(chuàng)建實例后通過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實現(xiàn)Ajax操作,對XMLHttpRequest對象進(jìn)行封裝,用于發(fā)出一個針對JSON數(shù)據(jù)的HTTP請求:
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ù)除了正常值意外,還可能是另一個Promise實例,如下:
var p1 = new Promise(function(resolve,reject){});
var p2 = new Promise(function(resolve,reject){/*code*/ resolve(p1)});
以上,p1的狀態(tài)會傳遞給p2,意指p1的狀態(tài)決定p2的狀態(tài)。如果p1的狀態(tài)是Pending,那么p2的回調(diào)函數(shù)會等待p1的狀態(tài)改變;如果p1的狀態(tài)已經(jīng)是Resolved或者rejected,那么p2的回調(diào)函數(shù)將會立即執(zhí)行。
方法講解:
Promise.prototype.then();
Promise實例具有then方法,也就是說,then方法是定義在原型對象Promise.prototype上的。它的作用是為Promise實例添加狀態(tài)改變時的回調(diào)函數(shù)。前面說過,then方法的第一個參數(shù)是Resolved狀態(tài)的回調(diào)函數(shù),第二個參數(shù)(可選)是Rejected狀態(tài)的回調(diào)函數(shù)。
then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。因此可以采用鏈?zhǔn)綄懛?,即then方法后面再調(diào)用另一個then方法。
getJSON("/posts.json").then(function(json){returnjson.post;}).then(function(post){// ...});
上面的代碼使用then方法,依次指定了兩個回調(diào)函數(shù)。第一個回調(diào)函數(shù)完成以后,會將返回結(jié)果作為參數(shù),傳入第二個回調(diào)函數(shù)。
采用鏈?zhǔn)降膖hen,可以指定一組按照次序調(diào)用的回調(diào)函數(shù)。這時,前一個回調(diào)函數(shù),有可能返回的還是一個Promise對象(即有異步操作),這時后一個回調(diào)函數(shù),就會等待該P(yáng)romise對象的狀態(tài)發(fā)生變化,才會被調(diào)用。
Promise.prototype.catch();
Promise.prototype.catch方法是.then(null, rejection)的別名,用于指定發(fā)生錯誤時的回調(diào)函數(shù)。
getJSON("/posts.json").then(function(posts){// ...}).catch(function(error){// 處理 getJSON 和 前一個回調(diào)函數(shù)運(yùn)行時發(fā)生的錯誤console.log('發(fā)生錯誤!',error);});
上面代碼中,getJSON方法返回一個Promise對象,如果該對象狀態(tài)變?yōu)镽esolved,則會調(diào)用then方法指定的回調(diào)函數(shù);如果異步操作拋出錯誤,狀態(tài)就會變?yōu)镽ejected,就會調(diào)用catch方法指定的回調(diào)函數(shù),處理這個錯誤。另外,then方法指定的回調(diào)函數(shù),如果運(yùn)行中拋出錯誤,也會被catch方法捕獲。
reject方法的作用,等同于拋出錯誤,但如果Promise狀態(tài)已經(jīng)變成Resolved,再拋出錯誤是無效的。
Promise對象的錯誤具有“冒泡”性質(zhì),會一直向后傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲。
getJSON("/post/1.json").then(function(post){returngetJSON(post.commentURL);}).then(function(comments){// some code}).catch(function(error){// 處理前面三個Promise產(chǎn)生的錯誤});
上面代碼中,一共有三個Promise對象:一個由getJSON產(chǎn)生,兩個由then產(chǎn)生。它們之中任何一個拋出的錯誤,都會被最后一個catch捕獲。
一般來說,不要在then方法里面定義Reject狀態(tài)的回調(diào)函數(shù)(即then的第二個參數(shù)),總是使用catch方法。
跟傳統(tǒng)的try/catch代碼塊不同的是,如果沒有使用catch方法指定錯誤處理的回調(diào)函數(shù),Promise對象拋出的錯誤不會傳遞到外層代碼,即不會有任何反應(yīng)。
Promise.resolve().catch(function(error){console.log('oh no',error);}).then(function(){console.log('carry on');});
需要注意的是,如上:catch方法返回的還是一個Promise對象,因此后面還可以接著調(diào)用then方法,如果沒有報錯,則會跳過catch方法。
Promise.all();
用于將多個Promise實例,包裝成一個新的Promise實例:var p = Promise.all([p1,p2,p3]);
上面代碼中,Promise.all方法接受一個數(shù)組作為參數(shù),p1、p2、p3都是Promise對象的實例,如果不是,就會先調(diào)用下面講到的Promise.resolve方法,將參數(shù)轉(zhuǎn)為Promise實例,再進(jìn)一步處理。(Promise.all方法的參數(shù)可以不是數(shù)組,但必須具有Iterator接口,且返回的每個成員都是Promise實例。)
p的狀態(tài)由p1、p2、p3決定,分成兩種情況。
(1)只有p1、p2、p3的狀態(tài)都變成fulfilled,p的狀態(tài)才會變成fulfilled,此時p1、p2、p3的返回值組成一個數(shù)組,傳遞給p的回調(diào)函數(shù)。
(2)只要p1、p2、p3之中有一個被rejected,p的狀態(tài)就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調(diào)函數(shù)。
下面是一個具體的例子。
// 生成一個Promise對象的數(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個Promise實例的數(shù)組,只有這6個實例的狀態(tài)都變成fulfilled,或者其中有一個變?yōu)閞ejected,才會調(diào)用Promise.all方法后面的回調(diào)函數(shù)。
Promise.race()
Promise.race方法同樣是將多個Promise實例,包裝成一個新的Promise實例。
varp=Promise.race([p1,p2,p3]);
上面代碼中,只要p1、p2、p3之中有一個實例率先改變狀態(tài),p的狀態(tài)就跟著改變。那個率先改變的Promise實例的返回值,就傳遞給p的回調(diào)函數(shù)。
Promise.race方法的參數(shù)與Promise.all方法一樣,如果不是Promise實例,就會先調(diào)用下面講到的Promise.resolve方法,將參數(shù)轉(zhuǎn)為Promise實例,再進(jìn)一步處理。
下面是一個例子,如果指定時間內(nè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方法無法返回結(jié)果,變量p的狀態(tài)就會變?yōu)閞ejected,從而觸發(fā)catch方法指定的回調(diào)函數(shù)。
Promise.resolve
有時需要將現(xiàn)有對象轉(zhuǎn)為Promise對象,Promise.resolve方法就起到這個作用。
varjsPromise=Promise.resolve($.ajax('/whatever.json'));
上面代碼將jQuery生成的deferred對象,轉(zhuǎn)為一個新的Promise對象。
Promise.resolve等價于下面的寫法。
Promise.resolve('foo')// 等價于newPromise(resolve=>resolve('foo'))
Promise.resolve方法的參數(shù)分成四種情況。
(1)參數(shù)是一個Promise實例
如果參數(shù)是Promise實例,那么Promise.resolve將不做任何修改、原封不動地返回這個實例。
(2)參數(shù)是一個thenable對象
thenable對象指的是具有then方法的對象,比如下面這個對象。
letthenable={then:function(resolve,reject){resolve(42);}};
Promise.resolve方法會將這個對象轉(zhuǎn)為Promise對象,然后就立即執(zhí)行thenable對象的then方法。
letthenable={then:function(resolve,reject){resolve(42);}};letp1=Promise.resolve(thenable);p1.then(function(value){console.log(value);// 42});
上面代碼中,thenable對象的then方法執(zhí)行后,對象p1的狀態(tài)就變?yōu)閞esolved,從而立即執(zhí)行最后那個then方法指定的回調(diào)函數(shù),輸出42。
(3)參數(shù)不是具有then方法的對象,或根本就不是對象
如果參數(shù)是一個原始值,或者是一個不具有then方法的對象,則Promise.resolve方法返回一個新的Promise對象,狀態(tài)為Resolved。
varp=Promise.resolve('Hello');p.then(function(s){console.log(s)});// Hello
上面代碼生成一個新的Promise對象的實例p。由于字符串Hello不屬于異步操作(判斷方法是它不是具有then方法的對象),返回Promise實例的狀態(tài)從一生成就是Resolved,所以回調(diào)函數(shù)會立即執(zhí)行。Promise.resolve方法的參數(shù),會同時傳給回調(diào)函數(shù)。
(4)不帶有任何參數(shù)
Promise.resolve方法允許調(diào)用時不帶參數(shù),直接返回一個Resolved狀態(tài)的Promise對象。
所以,如果希望得到一個Promise對象,比較方便的方法就是直接調(diào)用Promise.resolve方法。
varp=Promise.resolve();p.then(function(){// ...});
上面代碼的變量p就是一個Promise對象。
需要注意的是,立即resolve的Promise對象,是在本輪“事件循環(huán)”(event loop)的結(jié)束時,而不是在下一輪“事件循環(huán)”的開始時。
setTimeout(function(){console.log('three');},0);Promise.resolve().then(function(){console.log('two');});console.log('one');// one// two// three
上面代碼中,setTimeout(fn, 0)在下一輪“事件循環(huán)”開始時執(zhí)行,Promise.resolve()在本輪“事件循環(huán)”結(jié)束時執(zhí)行,console.log(’one‘)則是立即執(zhí)行,因此最先輸出。
Promise.reject()
Promise.reject(reason)方法也會返回一個新的Promise實例,該實例的狀態(tài)為rejected。它的參數(shù)用法與Promise.resolve方法完全一致。
varp=Promise.reject('出錯了');// 等同于varp=newPromise((resolve,reject)=>reject('出錯了'))p.then(null,function(s){console.log(s)});// 出錯了
上面代碼生成一個Promise對象的實例p,狀態(tài)為rejected,回調(diào)函數(shù)會立即執(zhí)行。
另兩個附加方法:
done()
Promise對象的回調(diào)鏈,不管以then方法或catch方法結(jié)尾,要是最后一個方法拋出錯誤,都有可能無法捕捉到(因為Promise內(nèi)部的錯誤不會冒泡到全局)。因此,我們可以提供一個done方法,總是處于回調(diào)鏈的尾端,保證拋出任何可能出現(xiàn)的錯誤。
asyncFunc().then(f1).catch(r1).then(f2).done();
它的實現(xiàn)代碼相當(dāng)簡單。
Promise.prototype.done=function(onFulfilled,onRejected){this.then(onFulfilled,onRejected).catch(function(reason){// 拋出一個全局錯誤setTimeout(()=>{throwreason},0);});};
從上面代碼可見,done方法的使用,可以像then方法那樣用,提供Fulfilled和Rejected狀態(tài)的回調(diào)函數(shù),也可以不提供任何參數(shù)。但不管怎樣,done都會捕捉到任何可能出現(xiàn)的錯誤,并向全局拋出。
finally();
finally方法用于指定不管Promise對象最后狀態(tài)如何,都會執(zhí)行的操作。它與done方法的最大區(qū)別,它接受一個普通的回調(diào)函數(shù)作為參數(shù),該函數(shù)不管怎樣都必須執(zhí)行。
下面是一個例子,服務(wù)器使用Promise處理請求,然后使用finally方法關(guān)掉服務(wù)器。
server.listen(0).then(function(){// run test}).finally(server.stop);
它的實現(xià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,都會執(zhí)行回調(diào)函數(shù)callback。
應(yīng)用
加載圖片
我們可以將圖片的加載寫成一個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ù)管理流程,遇到異步操作的時候,通常返回一個Promise對象。
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之中,有一個異步操作getFoo,它返回的就是一個Promise對象。函數(shù)run用來處理這個Promise對象,并調(diào)用下一個next方法。