1. Promise 的兩個特點
(1)對象的狀態不受外界影響。Promise 對象代表一個異步操作,有三種狀態:pending(進行中)、fulfilled(已成功)、rejected(已失敗)。只有異步操作的結果可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這個也是 Promise 這個名字的由來,代表只要“承諾”了便無法改變。
(2)一旦狀態確定,就不會再變,任何時候都可以得到這個結果。Promise 對象的狀態只有兩種情況,從 pending 到 fulfilled 和從 pending 到 rejected。只要這兩種情況發生了,狀態就凝固了,會一直保持這個結果,這時就成爲 resolved(已定型)。如果已定型,你再對 Promise 對象添加回調函數,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是如果你錯過了它再去監聽,是沒有結果的。
有了 Promise 對象,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise 對象提供統一的接口,使得異步操作更加容易。
但是,Promise 對象也有一定的缺點:首先,無法取消 promise,一旦建立它就會立即執行,無法中途取消。其次,如果不設置回調函數,promise 內部拋出的錯誤不會反應到外部。第三,當處於 pending 狀態時,無法得知目前進展到哪個階段(是剛剛開始還是即將完成)。
若某些事件反覆地發生,一般來說,使用 stream 模式是比部署 Promise 更好的選擇。
2. 基本語法
ES6 規定,Promise 對象是一個構造函數,用來生成 Promise 實例
const promise = new Promise(function(resolve,reject){//函數裏的兩個參數由 JavaScript 提供,不用自己部署
//some code
if(/*異步操作成功*/){
resolve(value);
}else{
reject(error);
}
});
Promise 實例生成以後,可以用 then 方法分別指定 resolved 狀態和 rejected 狀態的回調函數
then 中的兩個參數,第二個可選,都接受 promise 對象傳出的值作爲參數
promise.then(function(value){
//success
},function(error){
//failure
});
下面是一個簡單的 Promise 對象的例子
function timeout(ms){
return new Promise((resolve,reject) => {
setTimeout(resolve,ms,'done');
});
}
timeoue(100).then((value) =>{
console.log(value);
});
Promise 一旦建立就會立即執行,then 方法指定的回調函數,在當前腳本所有同步任務執行完後纔會執行。
下面是加載圖片的例子
function loadImageAsync(url){
return new Promise(function(resolve,reject){
const image = new Image();
image.onload = function(){
resolve(image);
};
image.onerror = function(){
reject(new Error("could not load image at"+url));
};
image.src = url;
});
}
resolve 函數和 reject 函數的參數會傳遞給回調函數,它們的參數也可以是一個 Promise 對象,即一個異步操作的結果是返回另一個異步操作。
const p1 = new Promise(function(resolve,reject){
setTimeout(() => reject(new Error('fail')),3000);
});
const p2 = new Promise(function(resolve,reject){
setTimeout(() => resolve(p1),1000);
});
p2
.then(resolve => console.log(resolve))
.catch(error => console.log(error))
//Error:fail
/*由於 p2 返回的是另一個 Promise 的狀態,導致 p2 自己的狀態無效了,由 p1 的狀態控制 p2 的狀態,
所以後面的 then 語句和 catch 語句都變成針對 p1 了。執行時,p1 3 秒之後變爲 rejected ,p2 的狀態
在 1 秒後改變,返回 p1 。又過了 2 秒後, p1 變爲 rejected,導致觸發 catch 方法指定的回調函數。*/
若 Promise 對象中,resolve 方法(reject 同)後面還有語句,則後面的語句會先執行,這是因爲 resolve 總會晚於本輪循環的同步任務。所以我們最好在 resolve 前面加上 return 語句,這樣就不會有意外。
//例1
const p = new Promise(function(resolve,reject){
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
//2
//1
//例2
const p = new Promise(function(resolve,reject){
return resolve(1);
console.log(2); //不會執行
});
3. Promise.prototype.then():then 方法的第一個參數是 resolved 狀態的回調函數,第二個參數(可選)是 rejected 狀態的回調參數。一般來說,不要在 then() 方法裏面定義 Rejected 狀態的回調函數(then 的第二個參數),總是使用 catch 方法比較好。
4. Promise.prototype.catch():這個方法是 .then(null,rejection) 或 .then(undefined,rejection) 的別名,用於指定發生錯誤時的回調函數。Promise 對象的錯誤具有 “冒泡” 性質,會一直向後傳遞,直到被捕獲爲止。
getJson('/posts.json').then(function(posts){
//...
}).catch(function(error){
//處理 getJson 和前一個回調函數運行時發生的錯誤
console.log("發生錯誤! ",error);
});
reject()的作用,等同於拋出錯誤。
const promise = new Promise(function(resolve,reject){
throw new Error('test');
});
promise.catch(function(error){
console.log(error);
});
//Error:test
//等同於
const promise = new Promise(function(resolve,reject){
try{
throw new Error('test');
}catch(e){
reject(e);
}
});
promise.catch(function(error){
console.log(error);
});
//等同於
const promise = new Promise(function(resolve,reject){
reject(new Error('test'));
});
promise.catch(function(error){
console.log(error);
});
跟傳統 try/catch 代碼塊不同的是,若沒有使用 catch()指定錯誤處理的回調函數,Promise 對象發生的錯誤不會傳遞到外層代碼,即不會做出任何反應。
const someAsyncThing = function(){
return new Promise(function(resolve,reject){
//下面一行信息報錯,因爲 x 沒有聲明
resolve(x + 2);
});
};
someAsyncThing().then(() => {console.log('everything is great')});
setTimeout(() => {console.log(123)},2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123
以上代碼說明,Promise 內部的錯誤不會影響到 Promise 外部的代碼,通俗的說法就是“ Promise 會喫掉錯誤”。這個腳本放在服務器執行,退出碼就是 0 (即表示執行成功)。
不過,Node.js 有一個專門監聽未捕獲的 reject 錯誤的事件:unhandleRejection,可以在監聽函數裏面拋出錯誤。第一個參數是錯誤對象,第二個參數是報錯的 Promise 實例,它可以用來了解發生錯誤的環境信息。
process.on('unhandleRejection',function(error,p){
throw error;
});
注意,Node 有計劃在未來廢除 unhandleRejection 事件。如果 Promise 內部有未捕獲的錯誤,會直接終止進程,並且進程的退出碼部位 0。
5. Promise.prototype.finally():用於指定不管 Promise 對象狀態最後如何,都會執行的操作。finally() 不接受任何參數。
以下一個例子是服務器使用 Promise 處理請求,最後使用 finally 方法關掉服務器。
server.listen(port)
.then(function(){
//...
})
.finally(server.stop);
6. Promise.all():用於將多個 Promise 實例,包裝成一個 Promise 實例。其參數可以不是數組,但必須是 Iterator 接口,且返回的每個成員都是 Promise 實例。
const p = Promise.all([p1,p2,p3]);
其中 p1,p2,p3 都是 Promise 實例,若不是實例,就會先調用 Promise.resolve(),將參數轉成 Promise 實例。
p 的狀態由 p1,p2,p3 決定
(1)只有當 p1,p2,p3 的狀態都爲 fulfilled , p 的狀態纔會爲 fulfilled,此時 p1,p2,p3 的返回值組成一個數組,傳遞給 p 的回調函數
(2)只要 p1,p2,p3 中有一個狀態爲 rejected,p 的狀態就會爲 rejected,此時第一個被 rejected 的實例的返回值,會傳遞給 p 的回調函數。
注意,若作爲參數的 Promise 實例,自己定義了 catch 方法,那麼它一旦被 rejected ,就不會出發 Promise.all() 的 catch 方法。
const p1 = new Promise(function(resolve,reject){
resolve("hello");
})
.then(result => result)
.catch(e => e);
const p2 = new Promise(function(resolve,reject){
throw new Error("報錯了");
})
.then(result => result)
.catch(e => e);
Promis.all([p1,p2])
.then(result => console.log(result))
.catch(e => console.log(e));
//["hello",Error:報錯了]
//p1 的狀態爲 resolved,p2 實例執行完 catch 後,狀態也會變成 resolved,故 Promise.all() 的狀態也是 resolved ,因此會調用 then()指定的回調函數。
若 p2 沒有自己的 catch 方法,就會調用 Promise.all() 的 catch 方法。
const p1 = new Promise(function(resolve,reject){
resolve("hello");
})
.then(result => result)
.catch(e => e);
const p2 = new Promise(function(resolve,reject){
throw new Error("報錯了");
})
.then(result => result);
Promis.all([p1,p2])
.then(result => console.log(result))
.catch(e => console.log(e));
//Error:報錯了
7. Promise.race():同樣是將多個 Promise 實例包裝成一個 Promise 實例。
Promise.race() 和 Promise.all() 一樣,如果參數不是 Promise 實例,會先調用 Promise.resolve()將參數轉爲 Promise 實例再進一步處理。
下面是一個例子,若在指定時間內沒有獲得結果,就將 Promise 的狀態變爲 rejected,否則變爲 resolved。
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function(resolve,reject){
setTimeout(() => reject(new Error('request timeout')),3000);
})
]);
p
.then(console.log)
.catch(console.error);
//若在 3 秒之內,fetch 方法無法返回結果,變量 p 的狀態就會變爲 rejected,從而觸發 catch 指定的方法回調函數
8. Promise.allSettled():接受一組 Promise 實例作爲參數,包裝成一個新的 Promise 實例。只有等到所有這些實例都返回結果,無論是 fulfilled 還是 rejected,包裝實例纔會結束。
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator();
下面是返回值用法的例子。
const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);
//過濾出成功的請求
const succesefulPromise = results.filter(p => p.status === "fulfilled");
//過濾出失敗的請求,請輸出原因
const error = results
.filter(p => p.status === "rejected")
.map(p => p.reason);
//每個對象都有 status 屬性,當 Promise 實例的狀態爲 fulfilled 時,status 有 value 屬性,Promise 狀態爲 rejected 時,status 有 reason 屬性。
有時候,我們不關心異步操作的結果,只關心異步操作有沒有結束。這時,Promise.allSettled() 就很有用,Promise.all() 無法做到這一點。
const urls = [/*...*/];
const requests = urls.map(x => fetch(x));
try{
await Promise.all(requests);
console.log("所有請求都成功");
}catch{
console.log("至少一個請求失敗,其他請求可能還沒結束");
}
9. Promise.any():接受一組 Promise 實例作爲參數,包裝成一個新的 Promise 實例。只要參數實例有一個是 fulfilled 狀態,那麼包裝實例就會變成 fulfilled 狀態,參數實例全部都爲 rejected 狀態時,包裝實例纔會爲 rejected 狀態。
Promise.any() 與 Promise.race() 很相似,只有一點不同,就是不會因爲某個 Promise 狀態爲 rejected 而結束。
Promise.any() 拋出的錯誤不是一個一般的錯誤,而是一個 AggregateError 實例,它相當於一個數組,每個成員對應一個被 rejected 的操作所拋出的錯誤。
new AggregateError() extends Array -> AggregateError
const err = new AggregateError();
err.push(new Error('first error'));
err.push(new Error('second error'));
throw err;
下面是一個例子
var resolved = Promise.resolve(42);
var rejected = Promise.reject(-1);
var rejected2 = Promise.reject(Infinity);
Promise.any([resolved,rejected,rejected2]).then(function(result){
console.log(result); //42
});
Promise.any([rejected,rejected2]).catch(function(result){
console.log(result); //[-1 Infinity]
});
10. Promise.resolve():將現有對象轉爲 Promise 實例。
Promise.resolve('foo');
//等同於
new Promise(resolve => resolve('foo'));
Promise.resolve() 的參數分成 4 中情況。
(1)若參數是 Promise 實例,那麼 Promise.resolve() 將不做任何修改,原封不動的返回這個實例。
(2)參數是一個 thenable 對象(即具有 then()的對象),將其轉爲 Promise 實例之後,立即執行 thenable 對象的 then 方法
let thenable = {
then: function(resolve,reject){
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value){
console.log(value); //42
});
(3)參數不是具有 then 方法的對象,或者根本不是對象,則 Promise.resolve() 返回一個新的對象,狀態爲 resolved。
const p = Promise.resolve('hello');
p.then(function(s){
console.log(s); //hello
});
(4)不帶任何參數,直接返回一個 resolved 狀態的 Promise 對象。若希望得到一個 Promise 對象,最簡單的方法就是用 Promise.resolve()
需要注意的是,立即 resolve()的 Promise 對象,是在本輪事件循環(event loop)的結束時執行,而不是在下一輪事件循環的開始時。
setTimeout(function(){
console.log("three");
},0);
Promise.resolve().then(function(){
console.log('two');
});
console.log('one');
//one
//two
//three
11. Promise.reject():也會返回一個 Promise 實例,狀態爲 rejected。
注意,Promise.reject() 的參數,會原封不動的作爲 reject 的理由,變成後續方法的參數,這一點與 Promise.resolve() 不同。
const thenable = {
then:function(resolve,reject){
reject('出錯了');
}
};
Promise.reject(thenable)
.catch(e =>{
console.log(e === thenable); //true
});
以上代碼中 Promise.reject() 的參數是一個 thenable 對象,執行以後,後面 catch 的參數不是 reject()拋出的 “出錯了”,而是 thenable 對象。
12. Promise.try():讓同步函數同步執行,異步函數異步執行,並且有統一的 API。
第一種寫法:
const f = () => console.log('now');
(async () => f())(); //立即執行函數
console.log('next');
//now
//next
以上代碼,若 f 是同步的,就會得到同步的結果,若 f 是異步的,就可以用 then 指定下一步,如
(async () => f())()
.then(...)
需要注意的是,async() 會喫掉 f() 拋出的錯誤,如果想要捕獲錯誤,要使用 promise.catch()
(async () => f())()
.then(...)
.catch(...)
第二種寫法:使用 new Promise()
const f = () => console.log('now');
(
() => new Promise(resolve,reject){
resolve => resolve(f());
}
)();
cosole.log('next');
//now
//next
鑑於這是一個很常見的需求,所以有一個提案,用 Promise.try() 代替以上的寫法
const f = () => console.log('now');
Promise.try(f);
console.log('next');
//now
//next