JavaScript引擎建立在單執行緒事件迴圈的概念上。單執行緒( Single-threaded )意味著同一時刻只能執行一段程式碼。所以引擎無須留意那些“可能”執行的程式碼。程式碼會被放置在作業佇列( job queue )中,每當一段程式碼準備被執行,它就會被新增到作業佇列。當 JS
引擎結束當前程式碼的執行後,事件迴圈就會執行佇列中的下一個作業.事件迴圈(event loop)是JS
引擎的一個內部處理執行緒,能監視程式碼的執行並管理作業佇列。關於事件迴圈可以閱讀這篇文章 ---- 一文梳理JavaScript 事件迴圈(Event Loop)
1. 為什麼要用Promise?
1.1 事件模型
當使用者點選一個按鈕或按下鍵盤上的一個鍵時,一個事件,例如 onclick
就被觸發了。該事件可能會對此互動進行響應,從而將一個新的作業新增到作業佇列的尾部。這就是 JavaScript 關於非同步程式設計的最基本形式。事件處理程式程式碼直到事件發生後才會被執行,此時它會擁有合適的上下文。例如:
let button = document.getElementById("my-btn");
button.onclick = function(event) {
console.log("Clicked");
};
事件可以很好地工作於簡單的互動,但將多個分離的非同步呼叫串聯在一起卻會很麻煩。此外,還需確保所有的事件處理程式都能在事件第一次觸發之前被繫結完畢。例如,若 button 在onclick
被繫結之前就被點選,那就不會有任何事發生。因此雖然在響應使用者互動或類似的低頻功能時,事件很有用,但它在面對更復雜的需求時仍然不夠靈活。
1.2 回撥函式
回撥函式模式類似於事件模型,因為非同步程式碼也會在後面的一個時間點才執行。不同之處在於需要呼叫的函
數(即回撥函式)是作為引數傳入的。
eadFile("example.txt", function(err, contents) {
if (err) {
throw err;
}
console.log(contents);
});
console.log("Hi!");
使用回撥函式模式,readFile()
會立即開始執行,並在開始讀取磁碟時暫停。這意味著console.log("Hi!")
會在 readFile()
被呼叫後立即進行輸出,要早於console.log(contents)
的列印操作。當 readFile()
結束操作後,它會將回撥函式以及相關引數作為一個新的作業新增到作業佇列的尾部。在之前的作業全部結束後,該作業才會執行。回撥函式模式要比事件模型靈活得多,因為使用回撥函式串聯多個呼叫會相對容易。
這種模式運作得相當好,但容易陷入了回撥地獄( callback hell ),這會在巢狀過多回撥函式時發生。當想要實現更復雜的功能時,回撥函式也會存在問題。如讓兩個非同步操作並行執行,並且在它們都結束後提醒你;同時啟動兩個非同步操作,但只採用首個結束的結果;在這些情況下,需要追蹤多個回撥函式並做清理操作, Promise 能大幅度改善這種情況。
2. Promise基礎
Promise 是非同步程式設計的一種解決方案,相比回撥函式和事件,更加強大。它由社群最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise
物件。
Promise 是為非同步操作的結果所準備的佔位符。函式可以返回一個 Promise,而不必訂閱一個事件或向函式傳遞一個回撥引數。
/ readFile 承諾會在將來某個時間點完成
let promise = readFile("example.txt");
每個 Promise 都會經歷一個短暫的生命週期,初始為pending ,這表示非同步操作尚未結束。一個狀態為pending的 Promise 也被認為是未決的( unsettled )。一旦非同步操作結束, Promise就會被認為是已決的(settled),並進入兩種可能狀態之一:
- fulfilled(已完成): Promise 的非同步操作已成功結束
- rejected(已拒絕):Promise 的非同步操作未成功結束,可能是一個錯誤,或由其他原因導致
內部的 [[PromiseState]] 屬性會被設定為 "pending" 、 "fulfilled" 或 "rejected" ,以反映 Promise 的狀態。該屬性並未在 Promise 物件上被暴露出來,因此你無法以程式設計方式判斷 Promise 到底處於哪種狀態。
2.1 Promise特質及優點
Promise
物件有以下兩個特點。
(1)物件的狀態不受外界影響。只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise
這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法改變。
(2)一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise
物件的狀態改變,只有兩種可能:從pending
變為fulfilled
和從pending
變為rejected
。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型)。如果改變已經發生了,再對Promise
物件新增回撥函式,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。
有了Promise
物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式。此外,Promise
物件提供統一的介面,使得控制非同步操作更加容易。
2.2 Promise缺點
- 無法取消
Promise
,一旦新建它就會立即執行,無法中途取消。 - 如果不設定回撥函式,
Promise
內部丟擲的錯誤,不會反應到外部 - 當處於
pending
狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)
3.建立Promise物件
3.1 建立未決的Promise
ES6 規定,Promise
物件是一個建構函式,用來生成Promise
例項。Promise 新建後就會立即執行。
Promise
建構函式接受一個函式作為引數,該函式的兩個引數分別是resolve
和reject
。它們是兩個函式,由 JavaScript 引擎提供,不用自己部署。
resolve
函式的作用是,將Promise
物件的狀態從“未完成”變為“成功”(即從 pending 變為 resolved),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;reject
函式的作用是,將Promise
物件的狀態從“未完成”變為“失敗”(即從 pending 變為 rejected),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value); // done
});
Promise
例項生成以後,可以用then
方法分別指定resolved
狀態和rejected
狀態的回撥函式。then
方法可以接受兩個回撥函式作為引數。第一個回撥函式是Promise
物件的狀態變為resolved
時呼叫,第二個回撥函式是Promise
物件的狀態變為rejected
時呼叫。這兩個函式都是可選的,不一定要提供。它們都接受Promise
物件傳出的值作為引數。
3.2 建立已決的Promise
使用Promise.resolve()和Promise.reject()方法能夠建立已決的Promise物件,前提是傳入引數不為pending態的Promise例項,並被Promise.resolve()方法呼叫。
(1) 引數為空
Promise.resolve()
方法呼叫時不帶引數,直接返回一個resolved
狀態的 Promise 物件。Promise.reject()
方法呼叫時不帶引數,直接返回一個rejected
狀態的 Promise 物件。
(2) 引數為Promise例項
注意:如果傳遞一個Promise給Promise.resolve(),則不做任何修改、原封不動地返回這個Promise。;傳遞給Promise.reject(),則會在原 Promise 上包裝出一個新的 Promise。示例如下所示:
// 傳入Promise狀態為resolved
let promise1 = Promise.resolve(43);
let promise2 = Promise.resolve(promise1); // Promise { 43 }
console.log(promise2===promise1); // true
promise2.then(function(value){
console.log(value) // 43
});
let promise3 = Promise.reject(promise1); // Promise { <rejected> Promise { 43 } }
promise3.catch(function(value){
console.log(value===promise1) // true
console.log(value) // Promise { 43 }
});
// 傳入Promise狀態為rejected
let promise4 = Promise.reject(44)
let promise5 = Promise.reject(promise4);
console.log(promise5); // Promise { <rejected> Promise { <rejected> 44 } }
promise5.catch(function(value){
console.log(value===promise4) // true
value.catch(function(v){
console.log(v) // 44
})
});
let promise6 = Promise.resolve(promise4); // Promise {<rejected>: 44}
console.log(promise6===promise4); // true
promise6.catch(function(v){
console.log(v); // 44
});
// 傳入Promise狀態為pending
let promise7 = new Promise(function(resolve, reject){
try{
resolve();
}catch (err){
reject(err);
}
});
promise7.then(function(){
console.log('promise7 resolved');
},function(err){
console.log('promise7 rejected');
});
let promise8 = Promise.resolve(promise7);
console.log(promise8===promise7); // true
promise8.then(function(value){
console.log(value); // undefined
})
let promise9 = Promise.reject(promise7);
console.log(promise9); // Promise { <rejected> Promise { undefined } }
promise9.catch(function(value){
console.log(value===promise7); // true
console.log(value); // Promise { undefined }
})
(3) 引數為非Promise的Thenable
Promise.resolve() 與 Promise.reject() 都能接受非 Promise 的 thenable 作為引數。
當一個物件擁有一個能接受 resolve 與 reject 引數的 then() 方法,該物件就會被認為是一個非 Promise 的 thenable ,就像這樣:
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
當傳入了非 Promise 的 thenable 時,Promise.resolve()方法會將其轉為Promise物件,然後立即執行thenable物件的then()方法。如下所示:
let thenable = {
then:function(resolve, reject){
resolve(43);
}
}
let p1 = Promise.resolve(thenable); // Promise { <pending> }
p1.then(function(value){
console.log(value); // 43
});
thenable = {
then:function(resolve, reject){
reject(44);
}
}
let p2 = Promise.resolve(thenable) // // Promise { <pending> }
p2.catch(function(value){
console.log(value); // 44
});
// p1,p2 等同於 new Promise(function(resolve, reject){
// try{
// resolve(43);
// } catch (err) {
// reject(44)
// }
// });
當傳入了非 Promise 的 thenable 時,Promise.reject()方法則會在thenable物件上包裝出一個Promise,狀態為rejected,呼叫該Promise的catch方法則其value引數為thenable物件。
let thenable = {
then:function(resolve, reject){
resolve(43);
}
}
let p1 = Promise.reject(thenable); // Promise { <rejected> { then: [Function: then] } }
console.log(p1)
p1.catch(function(value){
console.log(value=== thenable); // true
});
thenable = {
then:function(resolve, reject){
reject(44);
}
}
let p2 = Promise.reject(thenable) // Promise { <rejected> { then: [Function: then] } }
p2.catch(function(value){
console.log(value===thenable); // true
});
(4) 引數為不具有then方法
如果引數是一個原始值,或者是一個不具有then()
方法的物件,則Promise.resolve()
方法返回一個新的 Promise 物件,狀態為resolved
,Promise.reject()
方法返回一個狀態為rejected
的Promise物件。
4. 單非同步響應
Promise例項具有3個原型方法,用以平時處理單個非同步操作,如下所示:
- Promise.prototype.then
- Promise.prototype.catch
- Promise.prototype.finally
4.1 Promise.prototype.then
then
方法是定義在原型物件Promise.prototype
上的。作用是為 Promise 例項新增狀態改變時的回撥函式。前面說過,then
方法的第一個引數是resolved
狀態的回撥函式,第二個引數是rejected
狀態的回撥函式,它們都是可選的。
then
方法返回的是一個新的Promise
例項(注意,不是原來那個Promise
例項)。因此可以採用鏈式寫法,即then
方法後面再呼叫另一個then
方法。採用鏈式的then
,可以指定一組按照次序呼叫的回撥函式。這時,前一個回撥函式,有可能返回的還是一個Promise
物件(即有非同步操作),這時後一個回撥函式,就會等待該Promise
物件的狀態發生變化,才會被呼叫。
4.2 Promise.prototype.catch
Promise.prototype.catch()
方法等同於.then(null, rejection)
或.then(undefined, rejection)
,用於指定發生錯誤時的回撥函式。所以catch方法返回的也是一個新的Promise例項,也可以採用鏈式寫法。
如果非同步操作丟擲錯誤,Promise狀態變為rejected
,就會呼叫catch()
方法指定的回撥函式,處理這個錯誤。另外,then()
方法指定的回撥函式,如果執行中丟擲錯誤,也會被catch()
方法捕獲。
Promise 物件的錯誤具有“冒泡”性質,會一直向後傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch
語句捕獲。跟傳統的try/catch
程式碼塊不同的是,如果沒有使用catch()
方法指定錯誤處理的回撥函式Promise 物件丟擲的錯誤不會傳遞到外層程式碼,即不會有任何反應。
一般來說,不要在then()
方法裡面定義 Reject 狀態的回撥函式(即then
的第二個引數),總是使用catch
方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
4.3 Promise.prototype.finally
finally()
方法用於指定不管 Promise 物件最後狀態如何,都會執行的操作。該方法是 ES2018 引入標準的。finally
方法的回撥函式不接受任何引數,這意味著沒有辦法知道,前面的 Promise 狀態到底是fulfilled
還是rejected
。這表明,finally
方法裡面的操作,應該是與狀態無關的,不依賴於 Promise 的執行結果。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面程式碼中,不管promise
最後的狀態,在執行完then
或catch
指定的回撥函式以後,都會執行finally
方法指定的回撥函式。
finally
本質上是then
方法的特例。
promise
.finally(() => {
// 語句
});
// 等同於
promise
.then(
result => {
// 語句
return result;
},
error => {
// 語句
throw error;
}
);
上面程式碼中,如果不使用finally
方法,同樣的語句需要為成功和失敗兩種情況各寫一次。有了finally
方法,則只需要寫一次。
它的實現也很簡單。
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
上面程式碼中,不管前面的 Promise 是fulfilled
還是rejected
,都會執行回撥函式callback
。
5. 並行非同步響應
JavaScript中Promise有如下方法可並行處理多個非同步操作:
- Promise.all()
- Promise.race()
- Promise.allSettled()
- Promise.any()
5.1 Promise.all()
Promise.all()
方法接收單個可迭代物件(如陣列)作為引數,可迭代物件的元素都為Promise例項,若不是則呼叫Promise.resolve()
方法將其轉化為Promise例項,再進一步處理。
const p = Promise.all([p1,p2,p3])
p
的狀態由p1
、p2
、p3
決定,分成兩種情況。
(1)只有p1
、p2
、p3
的狀態都變成fulfilled
,p
的狀態才會變成fulfilled
,此時p1
、p2
、p3
的返回值組成一個陣列,傳遞給p
的回撥函式。
(2)只要p1
、p2
、p3
之中有一個被rejected
,p
的狀態就變成rejected
,此時第一個被reject
的例項的返回值,會傳遞給p
的回撥函式。
5.2 Promise.race()
Promise.race()
也接受一個包含需監視的 Promise 的可迭代物件,並返回一個新的 Promise。和Promise.all()方法不同的是,一旦來源Promise中有一個被完成,所返回的Promise就會立刻完成,那個率先完成得Promsie的返回值會傳遞給返回的Promise物件。
let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.race([p1, p2, p3]);
p4.then(function(value) {
console.log(value); // 42
});
5.3 Promise.allSettled()
Promise.allSettled()
方法接受一組 Promise 例項作為引數,包裝成一個新的 Promise 例項。只有等到所有這些引數例項都返回結果,不管是fulfilled
還是rejected
,包裝例項才會結束。該方法由 ES2020 引入。
該方法返回的新的 Promise 例項,一旦結束,狀態總是fulfilled
,不會變成rejected
。狀態變成fulfilled
後,Promise 的監聽函式接收到的引數是一個陣列,每個成員對應一個傳入Promise.allSettled()
的 Promise 例項。
let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
reject(44);
});
let p4 = Promise.allSettled([p1, p2, p3]);
p4.then(function(value) {
console.log(value);
console.log(p4);
});
0: {status: "fulfilled", value: 42}
1: {status: "fulfilled", value: 43}
2: {status: "rejected", reason: 44}
5.4 Promise.any()
ES2021 引入了Promise.any()
方法。該方法接受一組 Promise 例項作為引數,包裝成一個新的 Promise 例項返回。只要引數例項有一個變成fulfilled
狀態,
const p = Promise.any([p1,p2,p3]);
p
的狀態由p1
、p2
、p3
決定,分成兩種情況。
(1)只要p1
、p2
、p3
的狀態任意一個變成fulfilled
,p
的狀態就變成fulfilled
,並且首個fulfilled的Promise的返回值傳遞給p
的回撥函式。
(2)只有p1
、p2
、p3
全部被rejected
,p
的狀態就變成rejected
,並且丟擲一個AggregateError 錯誤。它相當於一個陣列,每個成員對應一個被rejected
的操作所丟擲的錯誤。
let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
reject(44);
});
let p = Promise.any([p1, p2, p3]);
p.then(function(value) {
console.log(value);
console.log(p);
});
results:
let p1 = Promise.reject(42);
let p2 = new Promise(function(resolve, reject) {
reject(43);
});
let p3 = new Promise(function(resolve, reject) {
reject(44);
});
let p = Promise.any([p1, p2, p3]);
p.then(function(value) {
console.log(value);
});
results:
6.小結
Promise
被設計用於改善JS中的非同步程式設計,與事件和回撥函式對比,在非同步操作中給我們提供了更多的控制權與組合型。Promise
具有三種狀態:掛起、已完成、已拒絕。一個Promise
起始於掛起態,並在成功時轉為完成態,或在失敗時轉為拒絕態。在這兩種情況下,處理函式都能被新增以表明Promise
何時被解決。then()
方法允許你繫結完成處理函式與拒絕處理函式,而 catch()
方法則只允許你繫結拒絕處理函式。並且Promise
能用多種方式串聯在一起,並在它們之間傳遞資訊。每個對 then()
的呼叫都建立並返回了一個新的Promise
,在前一個Promise
被決議時,新Promise
也會被決議。Promise
鏈可被用於觸發對一系列非同步事件的響應。除此之外,我們能夠使用Promsie.all()
/Promise.race()
/Promise.allSettled()
/Promise.any()
同時監聽多個Promise,並行性相應的響應。