學習Promise異步編程

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構造函數接受一個函數作爲參數,該函數的兩個參數分別是resolvereject。它們是兩個函數,由 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 對象,狀態爲resolvedPromise.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最後的狀態,在執行完thencatch指定的回調函數以後,都會執行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的狀態由p1p2p3決定,分成兩種情況。

(1)只有p1p2p3的狀態都變成fulfilledp的狀態纔會變成fulfilled,此時p1p2p3的返回值組成一個數組,傳遞給p的回調函數。

(2)只要p1p2p3之中有一個被rejectedp的狀態就變成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的狀態由p1p2p3決定,分成兩種情況。

(1)只要p1p2p3的狀態任意一個變成fulfilledp的狀態就變成fulfilled,並且首個fulfilled的Promise的返回值傳遞給p的回調函數。

(2)只有p1p2p3全部被rejectedp的狀態就變成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,並行性相應的響應。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章