天天看點

學習Promise異步程式設計

Promise被設計用于改善JS中的異步程式設計,與事件和回調函數對比,在異步操作中給我們提供了更多的控制權與組合型。Promise 具有三種狀态:挂起、已完成、已拒絕。一個 Promise 起始于挂起态,并在成功時轉為完成态,或在失敗時轉為拒絕态。在這兩種情況下,處理函數都能被添加以表明Promise 何時被解決。then() 方法允許你綁定完成處理函數與拒絕處理函數,而 catch()方法則隻允許你綁定拒絕處理函數。并且Promise能用多種方式串聯在一起,并在它們之間傳遞資訊。每個對 then() 的調用都建立并傳回了一個新的 Promise ,在前一個 Promise 被決議時,新 Promise 也會被決議。Promise 鍊可被用于觸發對一系列異步事件的響應。除此之外,我們能夠使用Promsie.all()/Promise.race()/Promise.allSettled()/Promise.any()同時監聽多個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

構造函數接受一個函數作為參數,該函數的兩個參數分别是

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}

           
學習Promise異步程式設計

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:

學習Promise異步程式設計
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:

學習Promise異步程式設計

6.小結

Promise

被設計用于改善JS中的異步程式設計,與事件和回調函數對比,在異步操作中給我們提供了更多的控制權與組合型。

Promise

具有三種狀态:挂起、已完成、已拒絕。一個

Promise

起始于挂起态,并在成功時轉為完成态,或在失敗時轉為拒絕态。在這兩種情況下,處理函數都能被添加以表明

Promise

何時被解決。

then()

方法允許你綁定完成處理函數與拒絕處理函數,而

catch()

方法則隻允許你綁定拒絕處理函數。并且

Promise

能用多種方式串聯在一起,并在它們之間傳遞資訊。每個對

then()

的調用都建立并傳回了一個新的

Promise

,在前一個

Promise

被決議時,新

Promise

也會被決議。

Promise

鍊可被用于觸發對一系列異步事件的響應。除此之外,我們能夠使用

Promsie.all()

/

Promise.race()

/

Promise.allSettled()

/

Promise.any()

同時監聽多個Promise,并行性相應的響應。