深入理解promise
對于現(xiàn)在的前端同學(xué)來說你不同promise你都不好意思出門了。對于前端同學(xué)來說promise已經(jīng)成為了我們的必備技能。
那么,下面我們就來說一說promise是什么,它能幫助我們解決什么問題,我們應(yīng)該如何使用它?
這是我個人對promise的理解。歡迎吐槽 :)
Promise是什么
promise的意思是承諾,有的人翻譯為許愿,但它們代表的都是未實(shí)現(xiàn)的東西,等待我們接下來去實(shí)現(xiàn)。
Promise最早出現(xiàn)在commnjs,隨后形成了Promise/A規(guī)范。在Promise這個技術(shù)中它本身代表以目前還不能使用的對象,但可以在將來的某個時間點(diǎn)被調(diào)用。使用Promise我們可以用同步的方式寫異步代碼。其實(shí)Promise在實(shí)際的應(yīng)用中往往起到代理的作用。例如,我們像我們發(fā)出請求調(diào)用服務(wù)器數(shù)據(jù),由于網(wǎng)絡(luò)延時原因,我們此時無法調(diào)用到數(shù)據(jù),我們可以接著執(zhí)行其它任務(wù),等到將來某個時間節(jié)點(diǎn)服務(wù)器響應(yīng)數(shù)據(jù)到達(dá)客戶端,我們即可使用promise自帶的一個回調(diào)函數(shù)來處理數(shù)據(jù)。
Promise能幫我們解決什么痛點(diǎn)
JavaScript實(shí)現(xiàn)異步執(zhí)行,在Promise未出現(xiàn)前,我們通常是使用嵌套的回調(diào)函數(shù)來解決的。但是使用回調(diào)函數(shù)來解決異步問題,簡單還好說,但是如果問題比較復(fù)雜,我們將會面臨回調(diào)金字塔的問題(pyramid of Doom)。
var a = function() {
console.log('a');
};
var b = function() {
console.log('b');
};
var c = function() {
for(var i=0;i<100;i++){
console.log('c')
}
};
a(b(c())); // 100個c -> b -> a
我們要桉順序的執(zhí)行a,b,c三個函數(shù),我們發(fā)現(xiàn)嵌套回調(diào)函數(shù)確實(shí)可以實(shí)現(xiàn)異步操作(在c函數(shù)中循環(huán)100次,發(fā)現(xiàn)確實(shí)是先輸出100個c,然后在輸出b,最后是a)。但是你發(fā)現(xiàn)沒這種實(shí)現(xiàn)可讀性極差,如果是幾十上百且回調(diào)函數(shù)異常復(fù)雜,那么代碼維護(hù)起來將更加麻煩。
那么,接下來我們看一下使用promise(promise的實(shí)例可以傳入兩個參數(shù)表示兩個狀態(tài)的回調(diào)函數(shù),第一個是resolve,必選參數(shù);第二個是reject,可選參數(shù))的方便之處。
var promise = new Promise(function(resolve, reject){
console.log('............');
resolve(); // 這是promise的一個機(jī)制,只有promise實(shí)例的狀態(tài)變?yōu)閞esolved,才會會觸發(fā)then回調(diào)函數(shù)
});
promise.then(function(){
for(var i=0;i<100;i++) {
console.log('c')
}
})
.then(function(){
console.log('b')
})
.then(function(){
console.log('a')
})
那么,為什么嵌套的回調(diào)函數(shù)這種JavaScript自帶實(shí)現(xiàn)異步機(jī)制不招人喜歡呢,因?yàn)樗目勺x性差,可維護(hù)性差;另一方面就是我們熟悉了jQuery的鏈?zhǔn)秸{(diào)用。所以,相比起來我們會更喜歡Promise的風(fēng)格。
promise的3種狀態(tài)
上面提到了promise的 resolved 狀態(tài),那么,我們就來說一下promise的3種狀態(tài),未完成(unfulfilled)、完成(fulfilled)、失?。╢ailed)。
在promise中我們使用resolved代表fulfilled,使用rejected表示fail。
ES6的Promise有哪些特性
promise的狀態(tài)只能從
未完成->完成,未完成->失敗且狀態(tài)不可逆轉(zhuǎn)。promise的異步結(jié)果,只能在完成狀態(tài)時才能返回,而且我們在開發(fā)中是根據(jù)結(jié)果來選擇來選擇狀態(tài)的,然后根據(jù)狀態(tài)來選擇是否執(zhí)行then()。
實(shí)例化的Promise內(nèi)部會立即執(zhí)行,then方法中的異步回調(diào)函數(shù)會在腳本中所有同步任務(wù)完成時才會執(zhí)行。因此,promise的異步回調(diào)結(jié)果最后輸出。示例代碼如下:
var promise = new Promise(function(resolve, reject) {
console.log('Promise instance');
resolve();
});
promise.then(function() {
console.log('resolved result');
});
for(var i=0;i<100;i++) {
console.log(i);
/*
Promise instance
1
2
3
...
99
100
resolved result
*/
上面的代碼執(zhí)行輸出結(jié)果的先后順序,曾經(jīng)有人拿到這樣一個面試題問過我,所以,這個問題還是要注意的。
resolve中可以接受另一個promise實(shí)例
resolve中接受另一個另一個對象的實(shí)例后,resolve本實(shí)例的返回狀態(tài)將會有被傳入的promise的返回狀態(tài)來取代。
reject狀態(tài)替換實(shí)例,代碼如下:
const p1 = new Promise(function (resolve, reject) {
cosole.log('2秒之后,調(diào)用返回p1的reject給p2');
setTimeout(reject, 3000, new Error('fail'))
})
const p2 = new Promise(function (resolve, reject) {
cosole.log('1秒之后,調(diào)用p1');
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// fail
resolve狀態(tài)替換實(shí)例,代碼如下:
const p1 = new Promise(function (resolve, reject) {
cosole.log('2秒之后,調(diào)用返回p1的resolve給p2');
setTimeout(resolve, 3000, 'success')
})
const p2 = new Promise(function (resolve, reject) {
cosole.log('1秒之后,調(diào)用p1');
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// success
注意:promise實(shí)例內(nèi)部的resolve也執(zhí)行的是異步回調(diào),所以不管resolve放的位置靠前還是靠后,都要等內(nèi)部的同步函數(shù)執(zhí)行完畢,才會執(zhí)行resolve異步回調(diào)。
new Promise((resolve, reject) => {
console.log(1);
resolve(2);
console.log(3);
}).then(result => {
console.log(result);
});
/*
1
3
2
*/
這個問題也在面試題中出現(xiàn)過,所以,要牢記。
promise和ajax如何結(jié)合使用
function PromiseGet (url) {
return new Promise( (resolve, reject) => {
let xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
if (this.status === 200) {
resolve(this.responseText, this)
} else {
let resJson = {
code: this.status,
response: this.response
}
reject(resJson, this)
}
}
}
xhr.send()
})
}
我們發(fā)現(xiàn)用promise技術(shù)結(jié)合ajax,只是在promise實(shí)例中引入ajax,在ajax請求處理的結(jié)果中使用了resolve和reject狀態(tài)。
前面我們說了resolve(),返回執(zhí)行then()代表完成,那么,reject()代表失敗,返回執(zhí)行catch(),同時運(yùn)行中拋出的錯誤也會執(zhí)行catch()
現(xiàn)在有一個非常好用的promise和Ajax結(jié)合的github項(xiàng)目Axios ,想要深入了解的同學(xué)可以研究下它的源碼。
promise.all()方法可以處理一個以promise實(shí)例為元素的數(shù)組
let promise = Promise.all([p1, p2, p3])
promise 的狀態(tài)由p1,p2,p3共同決定。當(dāng)它們都為resolve狀態(tài)時,promise狀態(tài)為true,它們的返回值組成一個數(shù)組,傳遞給promise;它們只要有一個的狀態(tài)為reject,就將該實(shí)例的返回值傳遞給promise
promise.race()方法也可以處理一個promise實(shí)例數(shù)組
但它和promise.all()不同,從字面意思上理解就是競速,那么理解起來上就簡單多了,也就是說在數(shù)組中的元素實(shí)例那個率先改變狀態(tài),就向下傳遞誰的狀態(tài)和異步結(jié)果。
將一個普通對象轉(zhuǎn)化為Promise對象
在開發(fā)中我們經(jīng)常會遇到.ajax()間的依賴使用,由于兩個或多個$.ajax()間是同步的,如果我們并排著寫實(shí)現(xiàn)不了依賴關(guān)系,所以,我們往往使用嵌套,但是對于ajax這樣復(fù)雜的結(jié)構(gòu),嵌套不是個好辦法,我們需要先將代碼抽象提取,做一下封裝,代碼如下:
/*
url:地址
data:數(shù)據(jù),在函數(shù)內(nèi)部會轉(zhuǎn)化成json。如果沒傳,表示用GET方法;如果傳了,表示用POST方法
*/
function ajax(url, data, callback) {
$.ajax({
url: url,
type: data == null ? 'GET' : 'POST',
dataType: "json",
data: data == null ? '' : JSON.stringify(data),
async: true,
contentType: "application/json",
success: function (data) {
callback(data);
},
error: function (XMLHttpRequest, textStatus) {
if (XMLHttpRequest.status == "401") {
window.parent.location = '...';
self.location = '...';
} else {
alert(XMLHttpRequest.responseText);
}
}
});
}
那么,我們應(yīng)該如何避免回調(diào)金字塔呢?很顯然,我們可以將它與promise結(jié)合,代碼如下:
function ajax(url, data, callback) =>
new Promise((resolve, reject) => {
$.ajax({
url: url,
type: data == null ? 'GET' : 'POST',
dataType: "json",
data: data == null ? '' : JSON.stringify(data),
async: true,
contentType: "application/json",
success: function (data) {
callback(data);
resolve();
},
error: function (XMLHttpRequest, textStatus) {
if (XMLHttpRequest.status == "401") {
window.parent.location = '...'
self.location = '...'
} else {
alert(XMLHttpRequest.responseText);
}
reject()
}
})
})
}
當(dāng)然,這是不熟悉jQuery的同學(xué),或者考慮長線Promise的,但是jQuery也為我們提供了按順序調(diào)用多個$.ajax()的方案,那就是deferred,它模擬了promise的實(shí)現(xiàn),有興趣的同學(xué)可以查看源碼,看它是如何實(shí)現(xiàn)的。實(shí)例代碼如下:
$.ajax({
url:'./a'
}).then(function(){
return $.ajax({ url:'./b' });
}).then(function(){
return $.ajax({ url:'./c' });
}).then(function(){
return $.ajax({ url:'./d' });
}).then(function(){
//TODO here
});
promise存在的問題
- promise一旦執(zhí)行,無法中途取消
- promise的錯誤無法在外部被捕捉到,只能在內(nèi)部進(jìn)行預(yù)判處理
- promise的內(nèi)如何執(zhí)行,監(jiān)測起來很難
正是因?yàn)檫@些原因,ES7引入了更加靈活多變的async,await來處理異步。
這個稍后,后續(xù)可能還會繼續(xù)修改,也歡迎各位批評指正。有問題或者有其他想法的可以在我的GitHub上pr。