什麼是Promise
Promise 是非同步程式設計的一種解決方案。ES6中已經提供了原生Promise
物件。一個Promise
物件會處於以下幾種狀態(fulfilled,rejected兩種狀態一旦確定後不會改變):
- 待定(pending): 初始狀態,既沒有被兌現,也沒有被拒絕。
- 已兌現(fulfilled): 意味著操作成功完成。
- 已拒絕(rejected): 意味著操作失敗。
基本用法
Promise
物件是一個建構函式,用來建立Promise
例項,它接收兩個引數resolve
和reject
。
resolve
的作用是將Promise
物件的狀態從pending
變為fulfilled
,在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去。reject
的作用是將Promise
物件的狀態從pending
變為rejected
,在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
const promise = new Promise(function(resolve, reject) {
// ...
if (/* 非同步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise
例項生成以後,使用then
方法分別指定fulfilled
狀態和rejected
狀態的回撥函式。
then
接收兩個引數,第一個是Promise
物件的狀態變為fulfilled
時的回撥函式,第二個是狀態變為rejected
時的回撥函式。catch
接收Promise
物件的狀態變為rejected
時的回撥函式。
promise.then(function (value){
// ....
},function (err){
// .... err
})
promise.then(function (value){
// ....
}).catch(function (err){
// ....
})
Promise的方法
Promise.prototype.then()
then
方法是定義在原型物件Promise.prototype
上,前面說過,它接收兩個可選引數,第一個引數是fulfilled
狀態的回撥函式,第二個引數是rejected
狀態的回撥函式。
then
方法返回的是一個新的Promise
例項,方便我們採用鏈式寫法。比如then
後面接著寫then
,當第一個回撥函式完成以後,會將返回結果作為引數,傳入第二個回撥函式。這種鏈式方式可以很方便的指定一組按照次序呼叫的回撥函式。
loadData().then(function (value){
return 3
}).then(function (num){
console.log("ok", num) // 3
})
Promise.prototype.catch()
catch
方法是用於指定發生錯誤時的回撥函式。如果非同步操作丟擲錯誤,狀態就會變為rejected
,就會呼叫catch()
方法指定的回撥函式,處理這個錯誤。
const promise = new Promise(function(resolve, reject) {
throw new Error('unkonw error'); // 丟擲錯誤狀態變為 -> reject
});
const promise = new Promise(function(resolve, reject) {
reject('unkonw error') // 使用reject()方法將狀態變為 -> reject
});
promise.catch(function(error) {
console.log(error);
});
Promise
物件的錯誤會一直向後傳遞,直到被捕獲為止。比如下面程式碼:catch
會捕獲loadData
和兩個then
裡面丟擲的錯誤。
loadData().then(function(value) {
return loadData(value);
}).then(function(users) {
}).catch(function(err) {
// 處理前面三個Promise產生的錯誤
});
如果我們不設定catch()
,當遇到錯誤時Promise
不會將錯誤丟擲外面,也就是不會影響外部程式碼執行。
const promise = new Promise(function(resolve, reject) {
resolve(a) // ReferenceError: a is not defined
});
promise.then(function(value) {
console.log('value is ', value)
});
setTimeout(() => { console.log('code is run') }, 1000); // code is run
Promise.prototype.finally()
finally()
方法不管 Promise 物件最後狀態如何,都會執行的操作。下面是我們使用 Promise 的一個常規結構。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
Promise.all()
Promise.all()
方法可以將多個 Promise 例項包裝成一個新的 Promise 例項返回。
const promise = Promise.all([p1, p2, p3]);
新promise
狀態來依賴於“傳入的promise
”。
- 只有當所有“傳入的
promise
”狀態都變成fulfilled
,它的狀態才會變成fulfilled
,此時“傳入的promise
”返回值組成一個陣列,傳遞給promise
的回撥函式。 - 如果“傳入的
promise
”之中有一個被rejected
,新promise
的狀態就會變成rejected
,此時第一個被reject
的promise
的返回值,會傳遞給promise
的回撥函式。
const promises = [1,2,3,4].map(function (id) {
return loadData(id);
});
Promise.all(promises).then(function (users) {
// ...
}).catch(function(err){
// ...
});
Promise.race()
Promise.race()
方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。
Promise.race()
方法的引數與Promise.all()
方法一樣。
const promise = Promise.race([p1, p2, p3]);
Promise.all()
和 Promise.race()
對比:
Promise.all()
,如果所有都執行成功則返回所有成功的promise
值,如果有失敗則返回第一個失敗的值。Promise.race()
,返回第一個執行完成的promise
值,它可能是fulfilled和rejected狀態。
這兩個方法的使用場景。
場景一,使用者登入社交網站主頁後,會同時非同步請求拉取使用者資訊,關注列表,粉絲列表,我們需要保證所有資料請求成功再進行渲染頁面,只要有一個資料不成功就會重定向頁面,這裡可以使用Promise.all
。
function initUserHome() {
Promise.all([
new Promise(getMe),
new Promise(getFollows),
new Promise(getFans)
])
.then(function(data){
// 顯示頁面
})
.catch(function(err){
// .... 重定向頁面
});
};
initUserHome();
場景二,假如我們在做一個搶票軟體,雖然請求了很多賣票渠道,每次只需返回第一個執行完成的Promise
,這裡可以使用Promise.race
。
function getTicket() {
Promise.race([
new Promise(postASell),
new Promise(postBSell),
new Promise(postCSell)
])
.then(function(data){
// 搶票成功
})
.catch(function(err){
// .... 搶票失敗,重試
});
};
getTicket();
Promise.allSettled()
使用Promise.all()
時,如果有一個Promise
失敗後,其它Promise
不會停止執行。
const requests = [
fetch('/url1'),
fetch('/url2'),
fetch('/url3'),
];
try {
await Promise.all(requests);
console.log('所有請求都成功。');
} catch {
console.log('有一個請求失敗,其他請求可能還沒結束。');
}
有的時候,我們希望等到一組非同步操作都結束了,再進行下一步操作。這時就需要使用Promise.allSettled()
,的它引數是一個陣列,陣列的每個成員都是一個 Promise 物件,返回一個新的 Promise 物件。它只有等到引數陣列的所有 Promise 物件都發生狀態變更(不管是fulfilled
還是rejected
),返回的 Promise 物件才會發生狀態變更。
const requests = [
fetch('/url1'),
fetch('/url2'),
fetch('/url3'),
];
await Promise.allSettled(requests);
console.log('所有請求完成後(包括成功失敗)執行');
Promise.any()
只要傳入的Promise
有一個變成fulfilled
狀態,新的Promise
就會變成fulfilled
狀態;如果所有傳入的Promise
都變成rejected
狀態,新的Promise
就會變成rejected
狀態。
Promise.any()
和Promise.race()
差不多,區別在於Promise.any()
不會因為某個 Promise
變成rejected
狀態而結束,必須等到所有引數 Promise
變成rejected
狀態才會結束。
回到Promise.race()
多渠道搶票的場景,如果我們需要保證要麼有一個渠道搶票成功,要麼全部渠道都失敗,使用Promise.any()
就顯得更合適。
function getTicket() {
Promise.any([
new Promise(postASell),
new Promise(postBSell),
new Promise(postCSell)
])
.then(function(data){
// 有一個搶票成功
})
.catch(function(err){
// .... 所有渠道都失敗了
});
};
getTicket();
Promise.resolve()
Promise.resolve()
方法將現有物件轉換為Promise
物件。等價於下面程式碼:
new Promise(resolve => resolve(1))
傳入的引數不同,處理
- 引數
Promise
例項,它將不做任何修改、原封不動地返回這個例項。 引數
thenable
物件,它會將這個物件轉為Promise
物件,然後就立即執行thenable
物件的then()
方法。let thenable = { then: function(resolve, reject) { resolve(1); } };
引數是普通值,返回一個新的 Promise 物件,狀態為
resolved
。const promise = Promise.resolve(1); promise.then(function (value) { console.log(value) // 1 });
- 無引數,直接返回一個
resolved
狀態的 Promise 物件。
Promise.reject()
Promise.reject(reason)
方法也會返回一個新的 Promise 例項,該例項的狀態為rejected
。
const promise = Promise.reject('unkonw error');
// 相當於
const promise = new Promise((resolve, reject) => reject('unkonw error'))
promise.then(null, function (s) {
console.log(s)
});
// unkonw error
簡單場景
非同步載入圖片:
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = url;
});
}
請求超時處理:
//請求
function request(){
return new Promise(function(resolve, reject){
// code ....
resolve('request ok')
})
}
function timeoutHandle(time){
return new Promise(function(resolve, reject){
setTimeout(function(){
reject('timeout');
}, time);
});
}
Promise.race([
request(),
timeoutHandle(5000)
])
.then(res=>{
console.log(res)
}).catch(err=>{
console.log(err)// timeout
})
小結
本文歸納了Promise
相關方法和一些簡單用法,歡迎留言交流。