promise簡介
Promise的出現(xiàn),原本是為了解決回調(diào)地獄的問題。所有人在講解Promise時,都會以一個ajax請求為例,此處我們也用一個簡單的ajax的例子來帶大家看一下Promise是如何使用的。
ajax請求的傳統(tǒng)寫法:
getData(method, url, successFun, failFun){
var xmlHttp = new XMLHttpRequest();
xmlHttp.open(method, url);
xmlHttp.send();
xmlHttp.onload = function () {
if (this.status == 200 ) {
successFun(this.response);
} else {
failFun(this.statusText);
}
};
xmlHttp.onerror = function () {
failFun(this.statusText);
};
}
改為promise后的寫法:
getData(method, url){
var promise = new Promise(function(resolve, reject){
var xmlHttp = new XMLHttpRequest();
xmlHttp.open(method, url);
xmlHttp.send();
xmlHttp.onload = function () {
if (this.status == 200 ) {
resolve(this.response);
} else {
reject(this.statusText);
}
};
xmlHttp.onerror = function () {
reject(this.statusText);
};
})
return promise;
}
getData('get','www.xxx.com').then(successFun, failFun)
很顯然,我們把異步中使用回調(diào)函數(shù)的場景改為了.then()、.catch()等函數(shù)鏈式調(diào)用的方式。基于promise我們可以把復(fù)雜的異步回調(diào)處理方式進行模塊化。
下面,我們就來介紹一下Promise到底是個什么東西?它是如何做到的?
Promise的狀態(tài)
其實promise原理說起來并不難,它內(nèi)部有三個狀態(tài),分別是pending,fulfilled和rejected 。
pending是對象創(chuàng)建后的初始狀態(tài),當(dāng)對象fulfill(成功)時變?yōu)閒ulfilled,當(dāng)對象reject(失敗)時變?yōu)閞ejected。且只能從pengding變?yōu)閒ulfilled或rejected ,而不能逆向或從fulfilled變?yōu)閞ejected 、從rejected變?yōu)閒ulfilled。如圖所示:
Promise實例方法介紹
Promise對象擁有兩個實例方法then()和catch()。
從前面的例子中可以看到,成功和失敗的回調(diào)函數(shù)我們是通過then()添加,在promise狀態(tài)改變時分別調(diào)用。promise構(gòu)造函數(shù)中通常都是異步的,所以then方法往往都先于resolve和reject方法執(zhí)行。所以promise內(nèi)部需要有一個存儲fulfill時調(diào)用函數(shù)的數(shù)組和一個存儲reject時調(diào)用函數(shù)的數(shù)組。
從上面的例子中我們還可以看到then方法可以接收兩個參數(shù),且通常都是函數(shù)(非函數(shù)時如何處理下一篇文章中會詳細介紹)。第一個參數(shù)會添加到fulfill時調(diào)用的數(shù)組中,第二個參數(shù)添加到reject時調(diào)用的數(shù)組中。當(dāng)promise狀態(tài)fulfill時,會把resolve(value)中的value值傳給調(diào)用的函數(shù)中,同理,當(dāng)promise狀態(tài)reject時,會把reject(reason)中的reason值傳給調(diào)用的函數(shù)。例:
var p = new Promise(function(resolve, reject){
resolve(5)
}).then(function(value){
console.log(value) //5
})
var p1 = new Promise(function(resolve, reject){
reject(new Error('錯誤'))
}).then(function(value){
console.log(value)
}, function(reason){
console.log(reason) //Error: 錯誤(…)
})
then方法會返回一個新的promise,下面的例子中p == p1將返回false,說明p1是一個全新的對象。
var p = new Promise(function(resolve, reject){
resolve(5)
})
var p1 = p.then(function(value){
console.log(value)
})
p == p1 // false
這也是為什么then是可以鏈式調(diào)用的,它是在新的對象上添加成功或失敗的回調(diào),這與jQuery中的鏈式調(diào)用不同。
那么新對象的狀態(tài)是基于什么改變的呢?是不是說如果p的狀態(tài)fulfill,后面的then創(chuàng)建的新對象都會成功;或者說如果p的狀態(tài)reject,后面的then創(chuàng)建的新對象都會失敗?
var p = new Promise(function(resolve, reject){
resolve(5)
})
var p1 = p.then(function(value){
console.log(value) // 5
}).then(function(value){
console.log('fulfill ' + value) // fulfill undefined
}, function(reason){
console.log('reject ' + reason)
})
上面的例子會打印出5和"fulfill undefined"說明它的狀態(tài)變?yōu)槌晒ΑD侨绻覀冊趐1的then方法中拋出異常呢?
var p = new Promise(function(resolve, reject){
resolve(5)
})
var p1 = p.then(function(value){
console.log(value) // 5
throw new Error('test')
}).then(function(value){
console.log('fulfill ' + value)
}, function(reason){
console.log('reject ' + reason) // reject Error: test
})
理所當(dāng)然,新對象肯定會失敗。
反過來如果p失敗了,會是什么樣的呢?
var p = new Promise(function(resolve, reject){
reject(5)
})
var p1 = p.then(undefined, function(value){
console.log(value) // 5
}).then(function(value){
console.log('fulfill ' + value) // fulfill undefined
}, function(reason){
console.log('reject ' + reason)
})
說明新對象狀態(tài)不會受到前一個對象狀態(tài)的影響。
再來看如下代碼:
var p = new Promise(function(resolve, reject){
reject(5)
})
var p1 = p.then(function(value){
console.log(value)
})
var p2 = p1.then(function(value){
console.log('fulfill ' + value)
}, function(reason){
console.log('reject ' + reason) // reject 5
})
我們發(fā)現(xiàn)p1的狀態(tài)變?yōu)閞ejected,從而觸發(fā)了then方法第二個參數(shù)的函數(shù)。這似乎與我們之前提到的有差異啊,p1的狀態(tài)受到了p的狀態(tài)的影響。
再來看一個例子:
var p = new Promise(function(resolve, reject){
resolve(5)
})
var p1 = p.then(undefined, function(value){
console.log(value)
})
var p2 = p1.then(function(value){
console.log('fulfill ' + value) // fulfill 5
}, function(reason){
console.log('reject ' + reason)
})
細心的人可能會發(fā)現(xiàn),該例子中then第一個參數(shù)是undefined,且value值5被傳到了p1成功時的回調(diào)函數(shù)中。上面那個例子中then的第二個參數(shù)是undefined,同樣reason值也傳到了p1失敗時的回調(diào)函數(shù)中。這是因當(dāng)對應(yīng)的參數(shù)不為函數(shù)時,會將前一promise的狀態(tài)和值傳遞下去。
promise含有一個實例方法catch,從名字上我們就看得出來,它和異常有千絲萬縷的關(guān)系。其實catch(onReject)方法等價于then(undefined, onReject),也就是說如下兩種情況是等效的。
new Promise(function(resolve, reject){
reject(new Error('error'))
}).then(undefined, function(reason){
console.log(reason) // Error: error(…)
})
new Promise(function(resolve, reject){
reject(new Error('error'))
}).catch(function(reason){
console.log(reason) // Error: error(…)
})
我們提到參數(shù)不為函數(shù)時會把值和狀態(tài)傳遞下去。所以我們可以在多個then之后添加一個catch方法,這樣前面只要reject或拋出異常,都會被最后的catch方法處理。
new Promise(function(resolve, reject){
resolve(5)
}).then(function(value){
taskA()
}).then(function(value){
taskB()
}).then(function(value){
taskC()
}).catch(function(reason){
console.log(reason)
})
Promise的靜態(tài)方法
Promise還有四個靜態(tài)方法,分別是resolve、reject、all、race,下面我們一一介紹。
除了通過new Promise()的方式,我們還有兩種創(chuàng)建Promise對象的方法:
Promise.resolve() 它相當(dāng)于創(chuàng)建了一個立即resolve的對象。如下兩段代碼作用相同:
Promise.resolve(5)
new Promise(function(resolve){
resolve(5)
})
它使得promise對象直接resolve,并把5傳到后面then添加的成功函數(shù)中。
Promise.resolve(5).then(function(value){
console.log(value) // 5
})
Promise.reject() 很明顯它相當(dāng)于創(chuàng)建了一個立即reject的對象。如下兩段代碼作用相同:
Promise.reject(new Error('error'))
new Promise(function(resolve, reject){
reject(new Error('error'))
})
它使得promise對象直接reject,并把error傳到后面catch添加的函數(shù)中
Promise.reject(new Error('error')).catch(function(reason){
console.log(reason) // Error: error(…)
})
Promise.all() 它接收一個promise對象組成的數(shù)組作為參數(shù),并返回一個新的promise對象。
當(dāng)數(shù)組中所有的對象都resolve時,新對象狀態(tài)變?yōu)閒ulfilled,所有對象的resolve的value依次添加組成一個新的數(shù)組,并以新的數(shù)組作為新對象resolve的value,例:
Promise.all([Promise.resolve(5),
Promise.resolve(6),
Promise.resolve(7)]).then(function(value){
console.log('fulfill', value) // fulfill [5, 6, 7]
}, function(reason){
console.log('reject',reason)
})
當(dāng)數(shù)組中有一個對象reject時,新對象狀態(tài)變?yōu)閞ejected,并以當(dāng)前對象reject的reason作為新對象reject的reason。
Promise.all([Promise.resolve(5),
Promise.reject(new Error('error')),
Promise.resolve(7),
Promise.reject(new Error('other error'))
]).then(function(value){
console.log('fulfill', value)
}, function(reason){
console.log('reject', reason) // reject Error: error(…)
})
那當(dāng)數(shù)組中,傳入了非promise對象會如何呢?
Promise.all([Promise.resolve(5),
6,
true,
'test',
undefined,
null,
{a:1},
function(){},
Promise.resolve(7)
]).then(function(value){
console.log('fulfill', value) // fulfill [5, 6, true, "test", undefined, null, Object, function, 7]
}, function(reason){
console.log('reject', reason)
})
我們發(fā)現(xiàn),當(dāng)傳入的值為數(shù)字、boolean、字符串、undefined、null、{a:1}、function(){}等非promise對象時,會依次把它們添加到新對象resolve時傳遞的數(shù)組中。
那數(shù)組中的多個對象是同時調(diào)用,還是一個接一個的依次調(diào)用呢?我們再看個例子
function timeout(time) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(time);
}, time);
});
}
console.time('promise')
Promise.all([
timeout(10),
timeout(60),
timeout(100)
]).then(function (values) {
console.log(values); [10, 60, 100]
console.timeEnd('promise'); // 107ms
});
由此我們可以看出,傳入的多個對象幾乎是同時執(zhí)行的,因為總的時間略大于用時最長的一個對象resolve的時間。
Promise.race() 它同樣接收一個promise對象組成的數(shù)組作為參數(shù),并返回一個新的promise對象。
與Promise.all()不同,它是在數(shù)組中有一個對象(最早改變狀態(tài))resolve或reject時,就改變自身的狀態(tài),并執(zhí)行響應(yīng)的回調(diào)。
Promise.race([Promise.resolve(5),
Promise.reject(new Error('error')),
Promise.resolve(7)]).then(function(value){
console.log('fulfill', value) // fulfill 5
}, function(reason){
console.log('reject',reason)
})
Promise.race([Promise.reject(new Error('error')),
Promise.resolve(7)]).then(function(value){
console.log('fulfill', value)
}, function(reason){
console.log('reject',reason) //reject Error: error(…)
})
且當(dāng)數(shù)組中有非異步Promise對象或有數(shù)字、boolean、字符串、undefined、null、{a:1}、function(){}等非Promise對象時,都會直接以該值resolve。
Promise.race([new Promise((resolve)=>{
setTimeout(()=>{
resolve(1)
},100)}),
Promise.resolve(5),
"test",
Promise.reject(new Error('error')),
Promise.resolve(7)]).then(function(value){
console.log('fulfill', value) // fulfill 5
}, function(reason){
console.log('reject',reason)
})
// fulfill 5
數(shù)組中第一個元素是異步的Promise,第二個是非異步Promise,會立即改變狀態(tài),所以新對象會立即改變狀態(tài)并把5傳遞給成功時的回調(diào)函數(shù)。
那么問題又來了,既然數(shù)組中第一個元素成功或失敗就會改變新對象的狀態(tài),那數(shù)組中后面的對象是否會執(zhí)行呢?
function timeout(time) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log(time)
resolve(time);
}, time);
});
}
console.time('promise')
Promise.race([
timeout(10),
timeout(60),
timeout(100)
]).then(function (values) {
console.log(values); [10, 60, 100]
console.timeEnd('promise'); // 107ms
});
// 結(jié)果依次為
// 10
// 10
// promise: 11.1ms
// 60
// 100
說明即使新對象的狀態(tài)改變,數(shù)組中后面的promise對象還會執(zhí)行完畢,其實Promise.all()中即使前面reject了,所有的對象也都會執(zhí)行完畢。規(guī)范中,promise對象執(zhí)行是不可以中斷的。
補充
promise對象即使立馬改變狀態(tài),它也是異步執(zhí)行的。如下所示:
Promise.resolve(5).then(function(value){
console.log('后打出來', value)
});
console.log('先打出來')
// 結(jié)果依次為
// 先打出來
// 后打出來 5
但還有一個有意思的例子,如下:
setTimeout(function(){console.log(4)},0);
new Promise(function(resolve){
console.log(1)
for( var i=0 ; i<10000 ; i++ ){
i==9999 && resolve()
}
console.log(2)
}).then(function(){
console.log(5)
});
console.log(3);
結(jié)果是 1 2 3 5 4,命名4是先添加到異步隊列中的,為什么結(jié)果不是1 2 3 4 5呢?這個涉及到Event loop,我之前文章講過。