ES6精華:Promise

Promise是異步編程的解決方案之一,相比傳統的回調和事件機制更為合理和強大。

1 場景舉例

某天,突發奇想,發了封郵件給木匠師傅,定製一個如此這般的傢俱。

木匠有求必應,即是說,郵件一旦發出就得到了他的承諾(Promise):在下一定盡力。

郵件中規定好了結果的通知方式:

成功了,直接將傢俱(res)郵遞(resolve)過來。

失敗了,直接將失敗的信息(err)發郵件(reject)過來。

let P = new Promise((resolve, reject) => { if (/*最終的結果*/) { resolve('傢俱'); // 成功,直接郵遞傢俱。 } else { reject('失敗的原因'); // 失敗,發郵件告知失敗原因。 }});

郵件發出等價於得到木匠的承諾P,之後,能做的只有等待(then)。

P.then(res => { console.log('成功,收到傢俱。此刻心情:開心。');}, err => { console.log('失敗,收到原因。此刻心情:失落。');});

2 行為特徵

2.1 狀態

每個Promise有三種狀態:進行中(pending)、已成功(resolved)和已失敗(rejected)。

創建即進入pending狀態,在傳入方法中一旦調用了resolve/reject方法,最終狀態便變成resolved/rejected。

一旦變成結果狀態,即更改成resolved/rejected,狀態便被冷凍,不能再被更改。

狀態容器

Promise實質是個狀態容器。

得到結果狀態後,任何時候都可以訪問到此狀態。

這與事件訂閱通知不同,如果訂閱發生在通知之後,訂閱是不起作用的。

狀態不可控

一旦創建Promise,便會立刻執行,無法取消。

處於pending狀態時,無法得知進程具體的信息,比如完成百分比(雖然可以自行設置回調進行通知)。

失敗的狀態

成功的狀態只能由resolve方法轉成。

失敗的狀態可以由reject方法轉成,也可以由拋出錯誤間接轉成。

三者都會正常的打印出失敗的信息。new Promise((resolve, reject) => { reject('error');}).catch(console.log); // errornew Promise((resolve, reject) => { a;}).catch(console.log); // ReferenceError: a is not definednew Promise((resolve, reject) => { throw 'error';}).catch(console.log); // Error: error 

錯誤的報告機制

如果失敗狀態沒有接收失敗的回調函數接收,Promise會拋出錯誤。

這裡的拋出錯誤,僅僅是在控制檯顯示之類的提示,不會終止程序的進程。

先打印出 'err' ,再報錯。new Promise((resolve, reject) => { reject();});new Promise((resolve, reject) => { reject('err');}).then(() => {}, console.log);

一旦Promise設置了失敗回調函數,即便是代碼執行錯誤,也會自行消化,不外報。

雖然 a 未被定義,但全程安靜,無槽點。new Promise((resolve, reject) => { a;}).then(() => {}, () => {});

2.2 執行順序

傳入方法

創建Promise的同時也會執行傳入方法。

傳入方法不會因為調用了resolve/reject便終止執行,所以更優的方式是retrun resolve/reject。

打印出 1 2 。new Promise((resolve, reject) => { console.log(1); resolve(); console.log(2);});

回調方法

立即得到結果的Promise,其回調函數依然會晚於本輪事件執行。

這種後執行不同於setTimeout的將執行函數push到執行棧,而是將執行函數放到本輪的末尾。

得到的結果是:1 2 3 4 5 6 。console.log(1);let p = new Promise((resolve, reject) => { console.log(2); resolve();});setTimeout(() => { console.log(5);});p.then(function() { console.log(4);});setTimeout(() => { console.log(6);});console.log(3);

2.3 結果參數

傳入reject的參數,一般是字符串或Error實例,表示拋出的錯誤。

傳入resolve的參數,一般是相應的JSON數據等,表示得到的數據。

傳入resolve的參數,還可以是另一個Promise實例。

這時,只有當內層的Promise結束後,外層的Promise才會結束。

過兩秒後,打印出 2000 。new Promise((resolve, reject) => { resolve(createPromise());}).then(console.log);function createPromise() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(2000); }, 2000); });}

在這種情況下,如果內層失敗,並不等於傳遞Error實例給resolve不同。

前者是內層Promise拋出了錯誤將被外層捕獲,後者僅僅是參數為一個Error實例。

內層失敗的信息,被外層捕獲。過兩秒,打印出 '2' 2000 。new Promise((resolve, reject) => { resolve(createPromise());}).then(res => { console.log('1', res);}, err => { console.log('2', err);});function createPromise() { return new Promise((resolve, reject) => { setTimeout(() => { reject(2000); }, 2000); });}

3 實例方法

3.1 then()

該方法可傳入兩個,分別對應成功/失敗時的回調函數。

該方法返回的是一個新的Promise對象,這也是可以使用鏈式(.then.then...)的原因。

let p1 = new Promise(resolve => resolve(2000));let p2 = p1.then(() => {}, () => {});console.log(p1 === p2); // false

return

鏈式中,後者的狀態取決於前者(成功/失敗)的回調函數中返回(return)的結果。

如果沒有返回,相當返回一個成功的狀態,值為undefined。

如果返回為Promise對象,後者的狀態由該對象的最終狀態決定。

如果返回為非Promise對象的數據,相當返回一個成功的狀態,值為此數據。

如果前者執行時拋出了錯誤,相當是返回一個失敗的狀態,值為此錯誤。

依次打印出:1 res 20002 res undefined3 res 30004 err 4000new Promise(resolve => resolve(2000)).then(res => { console.log('1 res', res);}).then(res => { console.log('2 res', res); return 3000;}).then(res => { console.log('3 res', res); return new Promise((resolve, reject) => { reject(4000); });}).then(console.log, err => { console.log('4 err', err);});

狀態的傳遞

在鏈式中,如果前者的狀態沒有被後者捕獲,會一直(像)冒泡到被捕獲為止。

狀態被捕獲後便消失,這之後的的狀態由當前then返回的狀態決定,之後重複。

依次打印出:2 res 20003 res 3000new Promise(resolve => resolve(2000)).then(null, err => { console.log('1 err', err);}).then(res => { console.log('2 res', res); return 3000;}).then(res => { console.log('3 res', res);});

3.2 catch()

用於指定發生錯誤時的回調函數,等價於:.then(null, callback)。

其表現與then一致,比如返回新的Promise,狀態的繼承和傳遞等等。

一般推薦使用catch而不是then的第二個方法接收錯誤。

因為catch可以捕獲then自身的錯誤,也更接近同步的寫法(try/catch)。

new Promise(() => {}).then(() => { ...}).catch(() => { ...});

3.3 finally()

用於Promise處理結束後的收尾工作。

傳入其的回調函數不會接受任何參數,意味著沒有辦法知道Promise的結果。

這也正表明,finally裡面的操作與狀態無關,不依賴Promise的處理結果。

其本質和catch一樣,也是then方法的變種。

不過其僅僅是狀態的傳遞者,只會返回原狀態,不會接收狀態和創建新的狀態。

p.finally(() => { // codes...});--- 等價於p.then(res => { // codes... return res; // 將原成功狀態返回}, err => { // codes... throw err; // 將原失敗狀態返回}); 

示例

在請求數據時,我們會顯示加載圖案,請求完成後無論結果都要隱藏此圖案。

一般,一個完整的 Promise 的結構會如下。showLoading = true;new Promise((resolve, reject) => { // 請求...}).then(res => { // 成功處理...}).catch(err => { // 失敗處理...}).finally(() => { // 重置一些狀態... showLoading = false;});

4 靜態方法

4.1 resolve()

此方法直接返回一個狀態為resolved,值為其參數的Promise。

Promise.resolve(res);--- 等價於new Promise(resolve => resolve(res));

4.2 reject()

此方法直接返回一個狀態為rejected,值為其參數的Promise。

Promise.reject(res);--- 等價於new Promise((resolve, reject) => reject(res));

4.3 all()

此方法用於將多個Promise實例,包裝成一個新的Promise實例。

其參數為一個數組,每一項應為Promise實例(不是則會使用Promise.resolve進行轉化)。

新Promise的狀態取決於傳入數組中的每一項的最終狀態。

如果有一項狀態變成rejected,新實例則為rejected,值為該項的返回值。

如果全部項都變成了resolved,新實例則為resolved,值為包含每一項返回值的數組。

三秒後,打印出:[1, 2, 3]。let pArr = [1, 2, 3].map(createPromise);Promise.all(pArr).then(console.log);function createPromise(num) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(num) }, num * 1000); });}

4.4 race()

此方法與all()基本相同,傳入的參數也是一個Promise數組。

不同的是,新Promise的最終狀態是由數組中第一個狀態改變的項(成功或失敗)決定的。

一秒後,打印出 1 。let pArr = [1, 2, 3].map(createPromise);Promise.race(pArr).then(console.log);function createPromise(num) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(num) }, num * 1000); });}

5 混合實戰

在實際項目中,有時需要處理多個相互關聯的異步腳本(多為數據請求)。

ES6之後async函數應該是最靈活方便的途徑,Promise在其中扮演基石的角色。

不過在這一小節,依舊會以Promise作為主要的解決辦法進行分析。

這裡是下面需要用到的共同方法。

// 創建異步。function createPromise(name) { return new Promise(resolve => { setTimeout(() => resolve({ [name]: `Data form ${name}` }), 1000); });}// 異步 A, B, C。function A(param) { return createPromise('A');}function B(param) { return createPromise('B');}function C(param) { return createPromise('C');}// 併發處理多個獨立的異步請求。function dealIndependentRequests(qArr, callback) { return new Promise((resolve, reject) => { let done = false; let resData = []; let leftNum = qArr.length; qArr.forEach((q, i) => { Promise.resolve(q).then(res => { !done && dealRequest(res, i, true); }).catch(err => { !done && dealRequest(err, i, false); }); }); function dealRequest(res, index, isSuccess) { if (callback) { done = callback(resData, res, index, isSuccess); } else { resData[index] = { res: res, isSuccess: isSuccess }; } if ( done || !(--leftNum) ) resolve(resData); } }); }

5.1

5.1.1

有三個請求數據的異步:A, B, C。

最終的數據必須同時結合三者的數據計算得出。

基於要求,直接使用Promise.all進行併發請求,等到所有信息到齊後結束。

大概一秒後,打印出:Get all data: [{...}, {...}, {...}]。Promise.all([A(), B(), C()]).then(res => { console.log(`Get all data:`, res);}).catch(err => { console.error(err);});

5.1.2

有三個請求數據的異步:A, B, C。

最終的數據必須同時結合A, B的數據計算得出,C只是修飾數據。

基於要求,使用Promise.all併發A, B請求,成功後再發C。

如果前者成功,再看C是否成功,之後使用不同方式處理得到最終數據。

大概兩秒後,打印出:[{…}, {…}] {C: "Data form C"}。Promise.all([A(), B()]).then(res => { C().then(c => { console.log(res, c); }) .catch(err => { console.log(res); });}).catch(err => { console.error(err);});

5.1.3

有三個請求數據的異步:A, B, C。

最終的數據必須基於結合A的數據計算得出,B, C起獨立的修飾作用。

基於要求,與上面的處理基本相同。

不過要在A的回調裡同時請求B, C,並使用狀態控制變量控制程序的進程。

大概兩秒後,打印出:End {A: "Data form A"} [{…}, {…}]。A().then(res => { dealIndependentRequests([B(), C()]) .then(subs => { console.log('End', res, subs); }) .catch(err => { console.log('End', res); });}).catch(err => { console.error(err);});

5.2

5.2.1

有三個請求異步:A, B, C。

B的請求需要發送A中的a信息。

C的請求需要發送B中的b信息。

基於要求,必須逐步請求A, B, C,而且前兩者任一出錯則停止。

大概三秒後,打印出:End {C: "Data form C"}。A().then(res => { return B(res.a);}).then(res => { return C(res.b); }).then(res => { console.log('End', res);}).catch(err => { console.log(err);});

5.2.2

有三個請求異步:A, B, C。

B的請求需要發送A中的a信息,即便A失敗也需要發送。

C的請求需要發送B中的b信息。

基於要求,與前者基本相同,只是即便A失敗了也會繼續請求。

大概三秒後,打印出:End {C: "Data form C"}。A().then(res => { return B(res.a);}).catch(err => { return B();}).then(res => { return C(res.b); }).then(res => { console.log('End', res);}).catch(err => { console.log(err);});

5.3

5.3.1

有三個請求異步:A, B, C。

需要找出所有異步結果中,包含某值的結果的集合。

基於要求,併發請求所有數據,一一驗證返回符合的結果集。

大概一秒後,打印出:[{B: "Data form B"}]dealIndependentRequests([A(), B(), C()], (data, res) => { if (res.B) data.push(res); return false;}).then(console.log).catch(console.log);

5.3.2

有三個請求異步:A, B, C。

只需要找到一個包含某值的結果。

基於要求,還是使用併發請求。

有任一請求符合預期時,結束並返回(暫不涉及取消請求操作)。

大概一秒後,打印出:[{B: "Data form B"}]dealIndependentRequests([A(), B(), C()], (data, res) => { if (res.B) return data.push(res); return false;}).then(console.log).catch(console.log);
ES6精華:Promise


分享到:


相關文章: