- Promise作為ES6中最重要的特性之一浙炼,我們有必要掌握并理解透徹泳秀。本文將由淺到深丐枉,講解Promise的基本概念與使用方法。
Promise 是一個構(gòu)造函數(shù)髓霞,接收一個函數(shù)為參數(shù),這個函數(shù)有兩個形參 resolve 和 reject畦戒,分別表示異步操作執(zhí)行成功后的回調(diào)函數(shù)和異步操作執(zhí)行失敗后的回調(diào)函數(shù)方库。其實(shí)這里用“成功”和“失敗”來描述并不準(zhǔn)確,按照標(biāo)準(zhǔn)來講障斋,resolve是將Promise的狀態(tài)置為fullfiled纵潦,reject是將Promise的狀態(tài)置為rejected。
-
Promise對象有以下兩個特點(diǎn)垃环。
(1)對象的狀態(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)閒ulfilled和從pending變?yōu)閞ejected垮兑。只要這兩種情況發(fā)生冷尉,狀態(tài)就凝固了,不會再變了甥角,會一直保持這個結(jié)果网严,這時就稱為 resolved(已定型)。如果改變已經(jīng)發(fā)生了嗤无,你再對Promise對象添加回調(diào)函數(shù)震束,也會立即得到這個結(jié)果。這與事件(Event)完全不同当犯,事件的特點(diǎn)是垢村,如果你錯過了它,再去監(jiān)聽嚎卫,是得不到結(jié)果的嘉栓。
new Promise(function(resolve, reject){ //做一些異步操作 setTimeout(function(){ console.log('執(zhí)行完成'); resolve('隨便什么數(shù)據(jù)'); }, 2000); }); 在上面的代碼中,我們執(zhí)行了一個異步操作拓诸,也就是setTimeout侵佃,2秒后,輸出“執(zhí)行完成”奠支,并且調(diào)用resolve方法馋辈。 運(yùn)行代碼,會在2秒后輸出“執(zhí)行完成”倍谜。注意迈螟!我只是new了一個對象,并沒有調(diào)用它尔崔,我們傳進(jìn)去的函數(shù)就已經(jīng)執(zhí)行了答毫,這是需要注意的一個細(xì)節(jié)。所以我們用Promise的時候一般是包在一個函數(shù)中季春,在需要的時候去運(yùn)行這個函數(shù)洗搂,如:
function runAsync(){ var p = new Promise(function(resolve, reject){ //做一些異步操作 setTimeout(function(){ console.log('執(zhí)行完成'); resolve('隨便什么數(shù)據(jù)'); }, 2000); }); return p; } runAsync()
- 這時候你應(yīng)該有兩個疑問:1.包裝這么一個函數(shù)有毛線用?2.resolve('隨便什么數(shù)據(jù)');這是干毛的?
- 我們繼續(xù)來講蚕脏。在我們包裝好的函數(shù)最后侦副,會return出Promise對象,也就是說驼鞭,執(zhí)行這個函數(shù)我們得到了一個Promise對象秦驯。還記得Promise對象上有then、catch方法吧挣棕?這就是強(qiáng)大之處了译隘,看下面的代碼:
runAsync().then(function(data){ console.log(data); //后面可以用傳過來的數(shù)據(jù)做些其他操作 ...... });
在runAsync()的返回上直接調(diào)用then方法,then接收一個參數(shù)洛心,是函數(shù)固耘,并且會拿到我們在runAsync中調(diào)用resolve時傳的的參數(shù)。運(yùn)行這段代碼词身,會在2秒后輸出“執(zhí)行完成”厅目,緊接著輸出“隨便什么數(shù)據(jù)”。
這時候你應(yīng)該有所領(lǐng)悟了法严,原來then里面的函數(shù)就跟我們平時的回調(diào)函數(shù)一個意思损敷,能夠在runAsync這個異步任務(wù)執(zhí)行完成之后被執(zhí)行。這就是Promise的作用了深啤,簡單來講拗馒,就是能把原來的回調(diào)寫法分離出來,在異步操作執(zhí)行完后溯街,用鏈?zhǔn)秸{(diào)用的方式執(zhí)行回調(diào)函數(shù)诱桂。
你可能會不屑一顧,那么牛逼轟轟的Promise就這點(diǎn)能耐呈昔?我把回調(diào)函數(shù)封裝一下挥等,給runAsync傳進(jìn)去不也一樣嗎,就像這樣:
function runAsync(callback){
setTimeout(function(){
console.log('執(zhí)行完成');
callback('隨便什么數(shù)據(jù)');
}, 2000);
}
runAsync(function(data){
console.log(data); //隨便什么數(shù)據(jù)
});
效果也是一樣的堤尾,還費(fèi)勁用Promise干嘛触菜。那么問題來了,有多層回調(diào)該怎么辦哀峻?如果callback也是一個異步操作,而且執(zhí)行完后也需要有相應(yīng)的回調(diào)函數(shù)哲泊,該怎么辦呢剩蟀?總不能再定義一個callback2,然后給callback傳進(jìn)去吧切威。而Promise的優(yōu)勢在于育特,可以在then方法中繼續(xù)寫Promise對象并返回,然后繼續(xù)調(diào)用then來進(jìn)行回調(diào)操作。
鏈?zhǔn)讲僮鞯挠梅?/h4>
*所以缰冤,從表面上看犬缨,Promise只是能夠簡化層層回調(diào)的寫法,而實(shí)質(zhì)上棉浸,Promise的精髓是“狀態(tài)”怀薛,用維護(hù)狀態(tài)、傳遞狀態(tài)的方式來使得回調(diào)函數(shù)能夠及時調(diào)用迷郑,它比傳遞callback函數(shù)要簡單枝恋、靈活的多。所以使用Promise的正確場景是這樣的:
runAsync1().then(function(data){
console.log(data);
return runAsync2();
})
.then(function(data){
console.log(data);
return runAsync3();
})
.then(function(data){
console.log(data);
});
這樣能夠按順序嗡害,每隔兩秒輸出每個異步回調(diào)中的內(nèi)容焚碌,在runAsync2中傳給resolve的數(shù)據(jù),能在接下來的then方法中拿到霸妹。運(yùn)行結(jié)果如下:
- 在then方法中十电,你也可以直接return數(shù)據(jù)而不是Promise對象,在后面的then中就可以接收到數(shù)據(jù)了叹螟,比如我們把上面的代碼修改成這樣:
runAsync1().then(function(data){
console.log(data);
return runAsync2();
})
.then(function(data){
console.log(data);
return '直接返回數(shù)據(jù)'; //這里直接返回數(shù)據(jù)
})
.then(function(data){
console.log(data);
});
那么輸出就變成了這樣:reject用法
到這里鹃骂,你應(yīng)該對“Promise是什么玩意”有了最基本的了解。那么我們接著來看看ES6的Promise還有哪些功能首妖。我們光用了resolve偎漫,還沒用reject呢,它是做什么的呢有缆?事實(shí)上象踊,我們前面的例子都是只有“執(zhí)行成功”的回調(diào),還沒有“失敗”的情況棚壁,reject的作用就是把Promise的狀態(tài)置為rejected杯矩,這樣我們在then中就能捕捉到,然后執(zhí)行“失敗”情況的回調(diào)袖外∈仿。看下面的代碼。
function getNumber(){
var p = new Promise(function(resolve, reject){
//做一些異步操作
setTimeout(function(){
var num = Math.ceil(Math.random()*10); //生成1-10的隨機(jī)數(shù)
if(num<=5){
resolve(num);
}
else{
reject('數(shù)字太大了');
}
}, 2000);
});
return p;
}
getNumber()
.then(
function(data){
console.log('resolved');
console.log(data);
},
function(reason, data){
console.log('rejected');
console.log(reason);
}
);
- getNumber函數(shù)用來異步獲取一個數(shù)字曼验,2秒后執(zhí)行完成泌射,如果數(shù)字小于等于5,我們認(rèn)為是“成功”了鬓照,調(diào)用resolve修改Promise的狀態(tài)熔酷。否則我們認(rèn)為是“失敗”了,調(diào)用reject并傳遞一個參數(shù)豺裆,作為失敗的原因拒秘。
-
運(yùn)行g(shù)etNumber并且在then中傳了兩個參數(shù),then方法可以接受兩個參數(shù),第一個對應(yīng)resolve的回調(diào)躺酒,第二個對應(yīng)reject的回調(diào)押蚤。所以我們能夠分別拿到他們傳過來的數(shù)據(jù)。多次運(yùn)行這段代碼羹应,你會隨機(jī)得到下面兩種結(jié)果:
catch的用法
我們知道Promise對象除了then方法揽碘,還有一個catch方法,它是做什么用的呢量愧?其實(shí)它和then的第二個參數(shù)一樣钾菊,用來指定reject的回調(diào),用法是這樣:
getNumber()
.then(function(data){
console.log('resolved');
console.log(data);
})
.catch(function(reason){
console.log('rejected');
console.log(reason);
});
效果和寫在then的第二個參數(shù)里面一樣偎肃。不過它還有另外一個作用:在執(zhí)行resolve的回調(diào)(也就是上面then中的第一個參數(shù))時煞烫,如果拋出異常了(代碼出錯了),那么并不會報錯卡死js累颂,而是會進(jìn)到這個catch方法中滞详。請看下面的代碼:
getNumber()
.then(function(data){
console.log('resolved');
console.log(data);
console.log(somedata); //此處的somedata未定義
})
.catch(function(reason){
console.log('rejected');
console.log(reason);
});
在resolve的回調(diào)中,我們console.log(somedata);而somedata這個變量是沒有被定義的紊馏。如果我們不用Promise料饥,代碼運(yùn)行到這里就直接在控制臺報錯了,不往下運(yùn)行了朱监。但是在這里岸啡,會得到這樣的結(jié)果:也就是說進(jìn)到catch方法里面去了,而且把錯誤原因傳到了reason參數(shù)中赫编。即便是有錯誤的代碼也不會報錯了巡蘸,這與我們的try/catch語句有相同的功能。
all的用法
Promise的all方法提供了并行執(zhí)行異步操作的能力擂送,并且在所有異步操作執(zhí)行完后才執(zhí)行回調(diào)悦荒。我們?nèi)耘f使用上面定義好的runAsync1、runAsync2嘹吨、runAsync3這三個函數(shù)搬味,看下面的例子:
Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
console.log(results);
});
用Promise.all來執(zhí)行,all接收一個數(shù)組參數(shù)蟀拷,里面的值最終都算返回Promise對象碰纬。這樣,三個異步操作的并行執(zhí)行的问芬,等到它們都執(zhí)行完后才會進(jìn)到then里面嘀趟。那么,三個異步操作返回的數(shù)據(jù)哪里去了呢愈诚?都在then里面呢,all會把所有異步操作的結(jié)果放進(jìn)一個數(shù)組中傳給then,就是上面的results炕柔。所以上面代碼的輸出結(jié)果就是:有了all酌泰,你就可以并行執(zhí)行多個異步操作,并且在一個回調(diào)中處理所有的返回數(shù)據(jù)匕累,是不是很酷陵刹?有一個場景是很適合用這個的,一些游戲類的素材比較多的應(yīng)用欢嘿,打開網(wǎng)頁時衰琐,預(yù)先加載需要用到的各種資源如圖片、flash以及各種靜態(tài)文件炼蹦。所有的都加載完后羡宙,我們再進(jìn)行頁面的初始化。
race的用法
all方法的效果實(shí)際上是「誰跑的慢掐隐,以誰為準(zhǔn)執(zhí)行回調(diào)」狗热,那么相對的就有另一個方法「誰跑的快,以誰為準(zhǔn)執(zhí)行回調(diào)」虑省,這就是race方法匿刮,這個詞本來就是賽跑的意思。race的用法與all一樣探颈,我們把上面runAsync1的延時改為1秒來看一下:
Promise
.race([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
console.log(results);
});
這三個異步操作同樣是并行執(zhí)行的熟丸。結(jié)果你應(yīng)該可以猜到,1秒后runAsync1已經(jīng)執(zhí)行完了伪节,此時then里面的就執(zhí)行了光羞。結(jié)果是這樣的:
Paste_Image.png
- 你猜對了嗎?不完全架馋,是吧狞山。在then里面的回調(diào)開始執(zhí)行時,runAsync2()和runAsync3()并沒有停止叉寂,仍舊再執(zhí)行萍启。于是再過1秒后,輸出了他們結(jié)束的標(biāo)志屏鳍。
- 這個race有什么用呢勘纯?使用場景還是很多的,比如我們可以用race給某個異步請求設(shè)置超時時間钓瞭,并且在超時后執(zhí)行相應(yīng)的操作驳遵,代碼如下:
//請求某個圖片資源
function requestImg(){
var p = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){
resolve(img);
}
img.src = 'xxxxxx';
});
return p;
}
//延時函數(shù),用于給請求計時
function timeout(){
var p = new Promise(function(resolve, reject){
setTimeout(function(){
reject('圖片請求超時');
}, 5000);
});
return p;
}
Promise
.race([requestImg(), timeout()])
.then(function(results){
console.log(results);
})
.catch(function(reason){
console.log(reason);
});
requestImg函數(shù)會異步請求一張圖片山涡,我把地址寫為"xxxxxx"堤结,所以肯定是無法成功請求到的唆迁。timeout函數(shù)是一個延時5秒的異步操作。我們把這兩個返回Promise對象的函數(shù)放進(jìn)race竞穷,于是他倆就會賽跑唐责,如果5秒之內(nèi)圖片請求成功了,那么遍進(jìn)入then方法瘾带,執(zhí)行正常的流程鼠哥。如果5秒鐘圖片還未成功返回,那么timeout就跑贏了看政,則進(jìn)入catch朴恳,報出“圖片請求超時”的信息。運(yùn)行結(jié)果如下:
下面是一個用Promise對象實(shí)現(xiàn)的 Ajax 操作的例子允蚣。
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const xhr= new XMLHttpRequest();
xhr.open("GET", url);
xhr.onreadystatechange = handler;
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
xhr.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出錯了', error);
});