Promise

ES6 新增的引用類型 Promise,可以通過(guò) new 操作符來(lái)實(shí)例化,創(chuàng)建時(shí)需要傳入執(zhí)行器(executor)函數(shù)作為參數(shù),如果不傳會(huì)報(bào)錯(cuò)。

let p = new Promise()
// Uncaught TypeError: Promise resolver undefined is not a function

// 傳入空函數(shù)
let p = new Promise(() => {});
console.log(p);
// Promise {<pending>}

Promise 是一個(gè)有三種狀態(tài)的對(duì)象:

  • 待定 pending
  • 兌現(xiàn) fulfilled(也可成為“解決”,resolved)
  • 拒絕 rejected

注意:fulfilled 中 ful 是一個(gè) L,兌現(xiàn)和拒絕都是被動(dòng)式。

基礎(chǔ)

通過(guò)執(zhí)行函數(shù)控制 Promise 狀態(tài)

由于狀態(tài)是私有的,所以只能通過(guò)內(nèi)部操作,即內(nèi)部操作在執(zhí)行函數(shù)中完成。執(zhí)行器函數(shù)有兩項(xiàng)職責(zé):初始化 Promise 的異步行為和控制狀態(tài)的最終轉(zhuǎn)換。其中,控制狀態(tài)的轉(zhuǎn)換是通過(guò)調(diào)用它的兩個(gè)函數(shù)參數(shù)實(shí)現(xiàn)的,分別為 resolve 和 reject。

執(zhí)行器函數(shù)是同步執(zhí)行的,因?yàn)閳?zhí)行器函數(shù)是 Promise 的初始化程序。

new Promise(() => {
  console.log('執(zhí)行器內(nèi)部');
  setTimeout(console.log, 0, 'executor');
});
console.log('主程序');
setTimeout(console.log, 0, 'promise initialized');
// 執(zhí)行器內(nèi)部
// 主程序
// executor
// promise initialized

無(wú)論 resolve 和 reject 中的哪個(gè)被調(diào)用,狀態(tài)轉(zhuǎn)換不可撤銷,繼續(xù)修改會(huì)靜默失敗。

let p = new Promise((res, rej) => {
  res();
  rej(); // 沒(méi)有效果
});
setTimeout(console.log, 0, p); // Promise <fulfilled>: undefined

Promise.resolve

Promise 并非一開始就必須處于待定狀態(tài),然后通過(guò)執(zhí)行器函數(shù)才能轉(zhuǎn)換為落定狀態(tài)。

通過(guò)調(diào)用 Promise.resolve() 靜態(tài)方法,可以實(shí)例化一個(gè)解決的 Promise。下面兩個(gè) Promise 實(shí)例實(shí)際上是一樣的:

let p1 = new Promise((res, rej) => res());
let p2 = Promise.resolve();

可以把解決的 Promise 的值傳給 Promise.resolve() 的第一個(gè)參數(shù)。使用這個(gè)靜態(tài)方法,實(shí)際上可以把任何職都轉(zhuǎn)換為一個(gè) Promise:

console.log(Promise.resolve());
// Promise <fulfilled>: undefined

console.log(Promise.resolve(3));
// Promise <fulfilled>: 3

console.log(Promise.resolve(Promise.resolve(3)));
// Promise <fulfilled>: 3

對(duì)這個(gè)靜態(tài)方法而言,如果傳入的參數(shù)本身是一個(gè) Promise,那么他的行為就類似于一個(gè)空包裝。因此 Promise.resolve() 可以說(shuō)是一個(gè)冪等方法:

let p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p));
// true

setTimeout(console.log, 0, Promise.resolve(Promise.resolve(p)));
// true

Promise.reject

與 Promise.resolve 類似,會(huì)實(shí)例化一個(gè)拒絕的 Promise 并拋出一個(gè)異步錯(cuò)誤(這個(gè)錯(cuò)誤不能被 try/catch 捕獲,而只能通過(guò)拒絕處理程序捕獲)。下面兩個(gè)實(shí)例其實(shí)是一樣的:

let p1 = new Promise((res, rej) => rej());
let p2 = Promise.reject();

這個(gè)拒絕的 Promise 的理由就是傳給 Promise.reject 的第一個(gè)參數(shù),這個(gè)參數(shù)也會(huì)傳給后續(xù)的拒絕處理程序:

let p = Promise.reject(3);
console.log(p); // Promise <rejected>: 3

p.then(null, (e) => setTimeout(console.log, 0, e)); // 3

關(guān)鍵在于,Promise.reject 沒(méi)有照搬 resolve 的冪等邏輯,如果給他傳一個(gè) Promise 對(duì)象,則這個(gè) Promise 會(huì)成為他返回拒絕 Promise 的理由:

setTimeout(console.log, 0, Promise.reject(Promise.resolve(3)));
// Promise {<rejected>: Promise}

同步/異步執(zhí)行的二元性

Promise 的設(shè)計(jì)很大程度上會(huì)導(dǎo)致一種完全不同于 JavaScript 的計(jì)算模式,其中包含了兩種模式下拋出錯(cuò)誤的情形:

try {
  throw new Error('foo');
} catch(e) {
  console.log(e); // Error: foo
}

try {
  Promise.reject(new Error('bar'));
} catch(e) {
  console.log(e);
}
// Promise {<rejected>: Error: bar}
// Uncaught (in promise) Error: bar

第一個(gè) try/catch 拋出并捕獲了錯(cuò)誤,第二個(gè)卻沒(méi)有捕獲到。乍一看這可能有點(diǎn)違反直覺,因?yàn)榇a中確實(shí)創(chuàng)建了一個(gè)拒絕的 Promise 實(shí)例,而這個(gè)實(shí)例也拋出了包含拒絕理由的錯(cuò)誤。這里的同步代碼之所以沒(méi)有捕獲 Promise 拋出的錯(cuò)誤,是因?yàn)樗麤](méi)有通過(guò)異步模式捕獲錯(cuò)誤。從這里就可以看出 Promise 真正的異步特性:它們是同步對(duì)象(在同步執(zhí)行模式中使用),但也是異步執(zhí)行模式的媒介。

這個(gè)例子,拒絕 Promise 的錯(cuò)誤并沒(méi)有跑到執(zhí)行同步代碼的線程里,而是通過(guò)瀏覽器異步消息隊(duì)列來(lái)處理。因此 try/catch 塊不能捕獲錯(cuò)誤。代碼一旦開始以異步模式執(zhí)行,則唯一與之交互的方式就是使用異步結(jié)構(gòu)——更具體地說(shuō),就是 Promise 的方法。

Promise.prototype.then

這個(gè)方法接收最多兩個(gè)參數(shù):onResolved 和 onRejected 處理程序,都是可選的。而且,傳給 then 的任何非函數(shù)類型的參數(shù)都會(huì)被靜默忽略。如果只提供 onRejected 參數(shù),那就要在 onResolved 參數(shù)位置上傳 undefined 有助于避免在內(nèi)存中創(chuàng)建多余的對(duì)象。

Promise.prototype.then 方法返回一個(gè)新的 Promise 實(shí)例:

let p1 = new Promise(() => {});
let p2 = p1.then(); // 如果調(diào)用 then 時(shí)不傳處理程序,則原樣往后傳
setTimeout(console.log, 0, p1);
// Promise <pending>
setTimeout(console.log, 0, p2);
// Promise <pending>
setTimeout(console.log, 0, p1 === p2);
// false

如果 Promise 實(shí)例基于 onResolved 處理程序的返回值構(gòu)建,該處理程序的返回值會(huì)通過(guò) Promise.resolve() 包裝來(lái)生成新 Promise。如果沒(méi)有提供這個(gè)處理程序,則 Promise.resolve() 就會(huì)包裝上一個(gè) Promise 解決之后的值。如果沒(méi)有顯示的返回語(yǔ)句,則 Promise.resolve() 會(huì)包裝默認(rèn)的返回值 undefined。

let p1 = Promise.resolve('foo');
let p2 = p1.then();
setTimeout(console.log, 0, p2); // Promise <fulfilled>: 'foo'

// 這些都一樣
let p3 = p1.then(() => undefined);
let p4 = p1.then(() => {});
let p5 = p1.then(() => Promise.resolve());

setTimeout(console.log, 0, p3); // Promise <fulfilled>: undefined
setTimeout(console.log, 0, p4); // Promise <fulfilled>: undefined
setTimeout(console.log, 0, p5); // Promise <fulfilled>: undefined

如果有顯示的返回值,則 Promise.resolve() 會(huì)包裝這個(gè)值:

let p1 = Promise.resolve('foo');
// 這些都一樣
let p6 = p1.then(() => 'bar');
let p7 = p1.then(() => Promise.resolve('bar'));

setTimeout(console.log, 0, p6); // Promise <fulfilled>: 'bar'
setTimeout(console.log, 0, p7); // Promise <fulfilled>: 'bar'

// Promise.resolve() 保留返回的 Promise
let p8 = p1.then(() => new Promise(() => {}));
let p9 = p1.then(() => Promise.reject());
// Uncaught (in promise): undefined

setTimeout(console.log, 0, p8); // Promise <pending>
setTimeout(console.log, 0, p9); // Promise <rejected>: undefined

拋出異常會(huì)返回拒絕的 Promise:(注意返回值)

let p1 = Promise.resolve('foo');
let p10 = p1.then(() => { throw 'baz' });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p10); // Promise <rejected>: baz

注意,返回錯(cuò)誤值不會(huì)觸發(fā)上面的拒絕行為,而會(huì)把錯(cuò)誤對(duì)象包裝在一個(gè)解決的 Promise 中:

let p1 = Promise.resolve('foo');
let p11 = p1.then(() => Error('qux'));
setTimeout(console.log, 0, p11); // Promise <fulfilled>: Error: qux

onRejected 處理程序也與之類似:onRejected 處理程序返回的值也會(huì)被 Promise.resolve() 包裝。乍一看可能有點(diǎn)違反直覺,但想一想,onRejected 處理程序的任務(wù)不就是捕獲異步錯(cuò)誤嗎?因此,拒絕處理程序在捕獲錯(cuò)誤猴不拋出異常是符合 Promise 的行為,應(yīng)該返回一個(gè)解決 Promise。

下面展示用 Promise.reject() 代替之前例子中的 Promise.resolve() 之后的結(jié)果:

let p1 = Promise.reject('foo');
let p2 = p1.then(); // 不傳處理程序則原樣后傳
setTimeout(console.log, 0, p2); // Promise <rejected>: 'foo'

// 這些都一樣
let p3 = p1.then(null, () => undefined);
let p4 = p1.then(null, () => {});
let p5 = p1.then(null, () => Promise.resolve());

setTimeout(console.log, 0, p3); // Promise <fulfilled>: 'foo'
setTimeout(console.log, 0, p4); // Promise <fulfilled>: 'foo'
setTimeout(console.log, 0, p5); // Promise <fulfilled>: 'foo'

// 這些都一樣
let p6 = p1.then(null, () => 'bar');
let p7 = p1.then(null, () => Promise.resolve('bar'));

setTimeout(console.log, 0, p6); // Promise <fulfilled>: 'bar'
setTimeout(console.log, 0, p7); // Promise <fulfilled>: 'bar'

// Promise.resolve() 保留返回的 Promise
let p8 = p1.then(null, () => new Promise(() => {}));
let p9 = p1.then(null, () => Promise.reject());

setTimeout(console.log, 0, p8); // Promise <pending>
setTimeout(console.log, 0, p9); // Promise <rejected>: undefined

let p10 = p1.then(null, () => { throw 'baz' });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p10); // Promise <rejected>: 'baz'

let p11 = p1.then(null, () => Error('qux'));
setTimeout(console.log, 0, p11); // Promise <fulfilled>: Error: 'qux'

Promise.prototype.catch

Promise.prototype.catch 用于給 Promise 添加拒絕處理程序。這個(gè)方法只接收一個(gè)參數(shù):onRejected 處理程序。事實(shí)上,這個(gè)方法就是一個(gè)語(yǔ)法糖,調(diào)用它就相當(dāng)于調(diào)用 Promise.prototype.then(null, onRejected)。

下面代碼展示同樣的情況:

let p = Promise.reject();
let onRejected = function (e) {
  setTimeout(console.log, 0, 'rejected');
}
// 這兩種添加拒絕處理程序的方式是一樣的:
p.then(null, onRejected); // rejected
p.catch(onRejected); // rejected

非重入 Promise 方法

當(dāng) Promise 進(jìn)入落定狀態(tài)時(shí),與該狀態(tài)相關(guān)的處理程序(onResolved/onRejected)僅僅會(huì)被排期,而非立即執(zhí)行。跟在添加這個(gè)處理程序的代碼之后的同步代碼一定會(huì)在處理程序之前先執(zhí)行。即使 Promise 一開始就是與附加處理程序關(guān)聯(lián)的狀態(tài),執(zhí)行順序也是這樣。這個(gè)特性有 JS 運(yùn)行時(shí)保證,被稱為“非重入”特性——non-reentrancy。

let p = Promise.resolve(); // 創(chuàng)建解決狀態(tài)的 Promise
// 添加解決處理程序
// 直覺上,這個(gè)處理程序會(huì)等 Promise 一解決就執(zhí)行
p.then(() => console.log('onResovled handler'));
// 同步輸出,證明 then() 已經(jīng)返回
console.log('then() returns');

// 實(shí)際的輸出:
// then() returns
// onResovled handler

在這個(gè)例子中,在一個(gè)解決 Promise 上調(diào)用 then() 會(huì)把 onResolved 處理程序推進(jìn)消息隊(duì)列。但這個(gè)處理程序在當(dāng)前線程上的同步代碼執(zhí)行完成前不會(huì)執(zhí)行。因此,跟在 then()后面的同步代碼一定先于處理程序執(zhí)行。

先添加處理程序(onResolved/onRejected)后解決 Promise 也是一樣的。

let synchronousResolve;
// 創(chuàng)建一個(gè) Promise 并將解決函數(shù)保存在一個(gè)局部變量中
let p = new Promise((res) => {
  synchronousResolve = function () {
    console.log('1: invoking resolve()');
    res();
    console.log('2: resolve returns');
  }
});

p.then(() => console.log('4: then() handler executes'));
synchronousResolve();
console.log('3: synchronousResolve() returns');

// 實(shí)際的輸出:
// 1: invoking resolve() 
// 2: resolve() returns 
// 3: synchronousResolve() returns 
// 4: then() handler executes

這個(gè)例子中,即使 Promise 狀態(tài)變化發(fā)生在添加處理程序之后,處理程序也會(huì)等到運(yùn)行的消息隊(duì)列讓它出列時(shí)才會(huì)執(zhí)行?!?: sync...” 打印在“4: then...”之前就說(shuō)明問(wèn)題了,為了清晰可以修改一下:

let synchronousResolve;
// 創(chuàng)建一個(gè) Promise 并將解決函數(shù)保存在一個(gè)局部變量中
let p = new Promise((res) => {
  synchronousResolve = function () {
    console.log('1: invoking resolve()');
    res();
    console.log('2: resolve returns');
    return '3: sync returns';
  }
});

p.then(() => console.log('4: then() handler executes'));
console.log('同步1:', p); // <pending>
console.log(synchronousResolve());
console.log('同步2:', p); // <fulfilled>

// 實(shí)際輸出:
// 同步1: Promise {<pending>}
// 1: invoking resolve()
// 2: resolve returns
// 3: sync returns
// 同步2: Promise {<fulfilled>: undefined}
// 4: then() handler executes

第15行都說(shuō)明了 Promise 的狀態(tài)改變了,但“4: then..”仍然是位于之后輸出。

非重入 Promise 適用于 onResolved/onRejected 處理程序、catch 處理程序和 finally 處理程序。

let p1 = Promise.resolve(); 
p1.then(() => console.log('p1.then() onResolved')); 
console.log('p1.then() returns'); 
let p2 = Promise.reject(); 
p2.then(null, () => console.log('p2.then() onRejected')); 
console.log('p2.then() returns'); 
let p3 = Promise.reject(); 
p3.catch(() => console.log('p3.catch() onRejected')); 
console.log('p3.catch() returns'); 
let p4 = Promise.resolve(); 
p4.finally(() => console.log('p4.finally() onFinally')); 
console.log('p4.finally() returns');

// p1.then() returns 
// p2.then() returns 
// p3.catch() returns 
// p4.finally() returns 
// p1.then() onResolved 
// p2.then() onRejected 
// p3.catch() onRejected 
// p4.finally() onFinally

鄰近處理程序的執(zhí)行順序

如果給期約添加了多個(gè)處理程序,當(dāng)期約狀態(tài)變化時(shí),相關(guān)處理程序會(huì)按照添加它們的順序依次執(zhí)行。無(wú)論是 then()、catch() 還是 finally() 添加的處理程序都是如此。

let p1 = Promise.resolve();
let p2 = Promise.reject();

p1.then(() => setTimeout(console.log, 0, 1));
p1.then(() => setTimeout(console.log, 0, 2));

p2.then(null, () => setTimeout(console.log, 0, 3));
p2.then(null, () => setTimeout(console.log, 0, 4));

p2.catch(() => setTimeout(console.log, 0, 5));
p2.catch(() => setTimeout(console.log, 0, 6));

p1.finally(() => setTimeout(console.log, 0, 7));
p1.finally(() => setTimeout(console.log, 0, 8));
// 1 2 3 4 5 6 7 8 順序多行輸出(這里節(jié)省篇幅就寫一行了)

傳遞解決值和拒絕理由

落定狀態(tài)后,Promise 會(huì)提供其解決值(如果兌現(xiàn))或其拒絕理由(如果拒絕)給相關(guān)狀態(tài)的處理程序。拿到返回值猴,就可以進(jìn)一步對(duì)這個(gè)值進(jìn)行操作。比如兩次網(wǎng)絡(luò)請(qǐng)求,第一次返回的 JSON 是發(fā)送第二次請(qǐng)求必須的數(shù)據(jù),那么該 JSON 應(yīng)該傳給 onResolved 處理程序繼續(xù)處理。當(dāng)然,失敗的網(wǎng)絡(luò)請(qǐng)求也應(yīng)該把 HTTP 狀態(tài)碼傳給 onRejected 處理程序。

在執(zhí)行函數(shù)中,解決的值和拒絕的理由是分別作為 resolve() 和 reject() 的第一個(gè)參數(shù)往后傳的。然后這些值又會(huì)傳給他們各自的處理程序,作為 onResolved 或 onRejected 處理程序的唯一參數(shù)。

let p1 = new Promise((res, rej) => res('foo'));
p1.then((value) => console.log(value)); // foo

let p2 = new Promise((res, rej) => rej('bar'));
p2.catch((reason) => console.log(reason)); // bar

Promise.resolve 和 Promise.reject 在被調(diào)用時(shí)就會(huì)接收解決值和拒絕理由。同樣地,它們返回的 Promise 也會(huì)像執(zhí)行器一樣把這些值傳給 onResolved 或 onRejected 處理程序:

let p1 = Promise.resolve('foo');
p1.then((value) => console.log(value)); // foo

let p2 = Promise.reject('bar');
p1.catch((reason) => console.log(reason)); // bar

拒絕 Promise 與拒絕錯(cuò)誤處理

拒絕 Promise 類似于 throw() 表達(dá)式,因?yàn)樗鼈兌即硪环N程序狀態(tài),即需要中斷或者特殊處理。在 Promise 的執(zhí)行函數(shù)或處理程序中拋出錯(cuò)誤會(huì)導(dǎo)致拒絕,對(duì)應(yīng)的錯(cuò)誤對(duì)象會(huì)成為拒絕理由。

因此以下這些 Promise 都會(huì)以一個(gè)錯(cuò)誤對(duì)象為由被拒絕:

let p1 = new Promise((res, rej) => rej(Error('p1 foo')));
let p2 = new Promise((res, rej) => { throw Error('p2 foo') });
let p3 = Promise.resolve().then(() => { throw Error('p3 foo') });
let p4 = Promise.reject(Error('p4 foo'));

setTimeout(console.log, 0, p1);
setTimeout(console.log, 0, p2);
setTimeout(console.log, 0, p3);
setTimeout(console.log, 0, p4);
// 也會(huì)拋出 4 個(gè)未捕獲的錯(cuò)誤

Promise 可以以任何理由拒絕,包括 undefined,但最好統(tǒng)一使用錯(cuò)誤對(duì)象。這樣做主要是因?yàn)閯?chuàng)建錯(cuò)誤對(duì)象可以讓瀏覽器捕獲錯(cuò)誤對(duì)象的棧追蹤信息,而這些信息對(duì)調(diào)試是非常關(guān)鍵的。例如前面的例子拋出的 4 個(gè)錯(cuò)誤棧追蹤信息如下:

<!-- 把上面的代碼寫在一個(gè)空的 .html 文件中 -->
<script>
  // 省略以上的示例代碼...(p1, p2, p3, p4)
  // 瀏覽器中會(huì)有如下輸出:
  /*
  promise-錯(cuò)誤捕獲.html:2 Uncaught (in promise) Error: p1 foo
    at promise-錯(cuò)誤捕獲.html:2:42
    at new Promise (<anonymous>)
    at promise-錯(cuò)誤捕獲.html:2:12
  promise-錯(cuò)誤捕獲.html:4 Uncaught (in promise) Error: p2 foo
    at promise-錯(cuò)誤捕獲.html:4:11
    at new Promise (<anonymous>)
    at promise-錯(cuò)誤捕獲.html:3:12
  promise-錯(cuò)誤捕獲.html:9 Uncaught (in promise) Error: p4 foo
    at promise-錯(cuò)誤捕獲.html:9:27
  promise-錯(cuò)誤捕獲.html:7 Uncaught (in promise) Error: p3 foo
    at promise-錯(cuò)誤捕獲.html:7:11
    */
</script>

所有錯(cuò)誤都是異步拋出且未處理的,通過(guò)錯(cuò)誤對(duì)象捕獲的棧追蹤信息展示了錯(cuò)誤發(fā)生的路徑。注意錯(cuò)誤的順序:Promise.resolve().then() 的錯(cuò)誤最后才出現(xiàn)(就是 Error: p3 foo),這是因?yàn)樗枰谶\(yùn)行時(shí)消息隊(duì)伍中添加處理程序,也就是說(shuō),在最終拋出未捕獲錯(cuò)誤之前它還會(huì)創(chuàng)建另一個(gè) Promise。

這個(gè)例子同樣揭示了異步錯(cuò)誤有意思的副作用。正常情況下,在通過(guò) throw 關(guān)鍵字拋出錯(cuò)誤時(shí),JS 運(yùn)行時(shí)的錯(cuò)誤處理機(jī)制會(huì)停止執(zhí)行拋出錯(cuò)誤之后的任何指令:

throw Error('foo');
console.log('bar'); // 這行不會(huì)執(zhí)行
// Uncaught Error: foo

但是在 Promise 拋出錯(cuò)誤時(shí),因?yàn)殄e(cuò)誤實(shí)際上是從消息隊(duì)列中異步拋出的,所以并不會(huì)阻止運(yùn)行時(shí)繼續(xù)執(zhí)行同步指令:

Promise.reject(Error('foo'));
console.log('bar');
// bar
// Uncaught (in promise) Error: foo

如本章前面的 Promise.reject() 示例,異步錯(cuò)誤只能通過(guò)異步的 onRejected 出來(lái)才行捕獲:

// 正確
Promise.reject(Error('foo')).catch(e => console.log(e));
// 錯(cuò)誤
try {
  Promise.reject(Error('foo'));
} catch(e) {
}

這不包括捕獲執(zhí)行函數(shù)中的錯(cuò)誤,在解決或拒絕 Promise 之前,仍然可以使用 try/catch 在執(zhí)行函數(shù)中捕獲錯(cuò)誤:

let p = new Promise((res, rej) => {
  try {
    throw Error('foo');
  } catch(e) {
    res('bar');
  }
});
setTimeout(console.log, 0, p); // Promise <fulfilled>: bar

then() 和 catch() 的 onRejected 處理程序在語(yǔ)義上相當(dāng)于 try/catch。出發(fā)點(diǎn)都是捕獲錯(cuò)誤之后將其隔離(2022年了看見這詞情不自禁發(fā)抖),同時(shí)不影響正常邏輯執(zhí)行。為此,onRejected 處理程序的任務(wù)應(yīng)該是在捕獲異步錯(cuò)誤之后返回一個(gè)解決的 Promise。

console.log('begin synchronous execution');
try {
  throw Error('foo');
} catch(e) {
  console.log('caught error', e);
}
console.log('continue synchronous execution');
// begin synchronous execution
// caught error Error: foo
// continue synchronous execution

let p = new Promise((res, rej) => {
  console.log('begin asynchronous execution');
  reject(Error('bar'));
}).catch(e => {
  console.log('caught error', e);
}).then(() => {
  console.log('continue asynchronous execution');
});

// begin asynchronous execution
// caught error Error: bar
// continue asynchronous execution

setTimeout(console.log, 0, p);
// Promise <fulfilled>: undefined

Promise 連鎖和 Promise 合成

多個(gè) Promise 組合在一起可以構(gòu)成強(qiáng)大的代碼邏輯。這種組合可以通過(guò)兩種方式實(shí)現(xiàn):連鎖與合成。前者是一個(gè)接一個(gè)地拼接,后者則是將多個(gè)組合為一個(gè)。

連鎖

把 Promise 逐個(gè)地串聯(lián)起來(lái)是一種非常有用的編程模式。之所以可以這樣,是因?yàn)槊總€(gè) Promise 實(shí)例的方法(then/catch/finally)都會(huì)返回一個(gè)新的 Promise,而這個(gè)新的家伙又有自己的實(shí)例方法。

let p = new Promise((res, rej) => {
  console.log(1);
  res();
});
p.then(() => console.log(2))
 .then(() => console.log(3))
 .then(() => console.log(4));
// 1
// 2
// 3
// 4

這個(gè)實(shí)現(xiàn)最終執(zhí)行了一連串“同步”任務(wù),正因如此,這樣沒(méi)啥用,畢竟分別使用4個(gè)同步函數(shù)也可以做到:

(() => console.log(1))(); 
(() => console.log(2))(); 
(() => console.log(3))(); 
(() => console.log(4))();

要真正執(zhí)行異步任務(wù),可以改寫,讓每個(gè)執(zhí)行器都返回一個(gè) Promise 實(shí)例。這樣可以讓每個(gè)后續(xù) promise 都等待之前的 promise,也就是串行化異步任務(wù)。比如像下面讓每個(gè) promise 在一定時(shí)間后解決:

let p1 = new Promise((res, rej) => {
  console.log('p1');
  setTimeout(res, 1000);
});
p1.then(() => new Promise((res, rej) => {
  console.log('p2');
  setTimeout(res, 1000);
})).then(() => new Promise((res, rej) => {
  console.log('p3');
  setTimeout(res, 1000);
})).then(() => new Promise((res, rej) => {
  console.log('p4');
  setTimeout(res, 1000);
}));
// p1 
// (1秒后)p2
// (2秒后)p3
// (3秒后)p4

將生成 Promise 的代碼提取到工廠函數(shù)中

function delayedResolve(str) {
  return new Promise((res, rej) => {
    console.log(str);
    setTimeout(res, 1000);
  });
}
delayedResolve('p1')
  .then(() => delayedResolve('p2'))
  .then(() => delayedResolve('p3'))
  .then(() => delayedResolve('p4'));
// p1 
// (1秒后)p2
// (2秒后)p3
// (3秒后)p4

每個(gè) Promise 的處理程序都會(huì)等待前一個(gè) Promise 解決,然后實(shí)例化一個(gè)新 Promise 并返回它,這種結(jié)構(gòu)可以簡(jiǎn)化地將異步任務(wù)串行化,解決之前依賴回調(diào)的難題,假如不使用 promise,那么前面的代碼可能就這樣寫了:

function delayedExecute(str, callback = null) {
  setTimeout(() => {
    console.log(str);
    callback && callback();
  }, 1000);
}
delayedExecute('p1', () => {
  delayedExecute('p2', () => {
    delayedExecute('p3', () => {
      delayedExecute('p4');
    });
  });
});
// 這里有些出入,因?yàn)閳?zhí)行器同步執(zhí)行的
// (1秒后)p1
// (2秒后)p2
// (3秒后)p3
// (4秒后)p4

Promise 圖

因?yàn)橐粋€(gè) Promise 可以有任意多個(gè)處理程序,所以連鎖可以構(gòu)建有向非循環(huán)圖的結(jié)構(gòu)。這樣每個(gè) promise 都是圖中的一個(gè)節(jié)點(diǎn),而使用實(shí)例方法添加的處理程序則是有向頂點(diǎn)。因?yàn)閳D中每個(gè)節(jié)點(diǎn)都會(huì)等待前一個(gè)節(jié)點(diǎn)落定,所以圖的方向就是 promise 的解決或拒絕順序。

// 下面的例子展示了一種 promise 有向圖,也就是二叉樹:
//     A 
//    / \ 
//   B   C 
//  /\   /\ 
//  D E  F G

let A = new Promise((res, rej) => {
  console.log('A');
  res();
});

let B = A.then(() => console.log('B'));
let C = A.then(() => console.log('C'));

B.then(() => console.log('D'));
B.then(() => console.log('E'));
C.then(() => console.log('F'));
C.then(() => console.log('G'));
// A
// B
// C
// D
// E
// F
// G

注意輸出語(yǔ)句是對(duì)二叉樹的層序遍歷。promise 的處理程序是按照他們添加的順序執(zhí)行的。

由于 promise 的處理程序先添加到消息隊(duì)列,然后才逐個(gè)執(zhí)行,因此構(gòu)成了層序遍歷。樹只是圖的一種形式。考慮到根節(jié)點(diǎn)不一定唯一,且多個(gè) promise 也可以組成一個(gè) promise,所以有向非循環(huán)圖是體現(xiàn) promise 連鎖可能性的最準(zhǔn)確表達(dá)。

Promise.all 和 Promise.race

Promise 類提供兩個(gè)將多個(gè) promise 實(shí)例組合成一個(gè) promise 的靜態(tài)方法。而合成 promise 的行為取決于內(nèi)部 promise 的行為。

Promise.all

該方法創(chuàng)建的 promise 會(huì)在一組 promise 全部解決之后再解決。接收一個(gè)可迭代對(duì)象,返回一新 promise:

let p1 = Promise.all([
  Promise.resolve(),
  Promise.resolve()
]);
// 可迭代對(duì)象中的元素會(huì)通過(guò) Promise.resolve() 轉(zhuǎn)換為 promise
let p2 = Promise.all([3, 4]);
// 空的可迭代對(duì)象等價(jià)于 Promise.resolve()
let p3 = Promise.all([]);

// 無(wú)效
let p4 = Promise.all();
// TypeError: : undefined is not iterable (cannot read property Symbol(Symbol.iterator))

合成的 promise 只會(huì)在每個(gè)包含的 promise 都解決之后才解決:

let p = Promise.all([
  Promise.resolve(),
  new Promise((res, rej) => setTimeout(res, 1000))
]);
setTimeout(console.log, 0, p); // Promise <pending>

p.then(() => setTimeout(console.log, 0, 'all() resolved'));

// all() resolved(大約1秒后)

如果至少有一個(gè)包含的 promise 待定,則合成的 promise 也會(huì)待定。如果有一個(gè)包含的 promise 拒絕,則合成的 promise 也會(huì)拒絕:

// 永遠(yuǎn)待定
let p1 = Promise.all([ new Promise(() => {}) ]);
setTimeout(console.log, 0, p1);

let p2 = Promise.all([
  Promise.resolve(),
  Promise.reject(),
  Promise.resolve(),
]);
setTimeout(console.log, 0, p2); // Promise <rejected>

// Uncaught (in promise) undefined

如果所有 promise 都成功解決,則合成 promise 的解決值就是所有包含 promise 解決值的數(shù)組,按照迭代順序:

let p = Promise.all([
  Promise.resolve(3),
  Promise.resolve(),
  Promise.resolve(4),
]);
p.then(values => setTimeout(console.log, 0, values));
// [3, undefined, 4]

如果有 promise 拒絕,則第一個(gè)拒絕的 promise 會(huì)將自己的理由作為合成 promise 的拒絕理由。之后再拒絕的 promise 不會(huì)影響最終 promise 的拒絕理由。不過(guò),這并不影響所有包含 promise 正常的拒絕操作。合成的 promise 會(huì)靜默處理所有包含 promise 的拒絕操作。

let p = Promise.all([ 
 Promise.reject(3), 
 new Promise((resolve, reject) => setTimeout(reject, 1000)) 
]);
p.catch((reason) => setTimeout(console.log, 0, reason)); // 3

// 沒(méi)有未處理的錯(cuò)誤

Promise.race

該靜態(tài)方法返回一個(gè)包裝 promise,是一組集合中最先解決或拒絕的 promise 的鏡像。這個(gè)方法接收一個(gè)可迭代對(duì)象,返回一個(gè)新 promise:

let p1 = Promise.race([
  Promise.resolve(),
  Promise.resolve()
]);
// 可迭代對(duì)象中的元素會(huì)通過(guò) Promise.resolve 轉(zhuǎn)換為 promise
let p2 = Promise.race([3, 4]);
// 空的可迭代對(duì)象等等價(jià)于 new Promise(() => {})
let p3 = Promise.race([]); // Promise <pending> 和 all 不一樣(<fulfilled>)
// 無(wú)效
let p4 = Promise.race();
// TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))

Promise.race 不會(huì)對(duì)解決或拒絕的 promise 區(qū)別對(duì)待。無(wú)論是解決還是拒絕,只要是第一個(gè)落定的 promise,Promise.race 就會(huì)包裝其解決值或拒絕理由并返回新 Promise:

// 解決先發(fā)生,超時(shí)后的拒絕被忽略
let p1 = Promise.race([
  Promise.resolve(),
  new Promise((res, rej) => setTimeout(rej, 1000))
]);
setTimeout(console.log, 0, p1); // Promise <fulfilled>: 3

// 拒絕先發(fā)生,超時(shí)后的解決被忽略
let p2 = Promise.race([
  Promise.reject(4),
  new Promise((res, rej) => setTimeout(res, 1000))
]);
setTimeout(console.log, 0, p2); // Promise <rejected>: 4

// 迭代順序決定了落定順序
let p3 = Promise.race([
  Promise.resolve(5),
  Promise.resolve(6),
  Promise.resolve(7),
]);
setTimeout(console.log, 0, p2); // Promise <fulfilled>: 5

如果有一個(gè) promise 拒絕,只要它是第一個(gè)落定的,就會(huì)成為拒絕合成 promise 的理由。之后再拒絕的 promise 不會(huì)影響最終 promise 的拒絕理由。不過(guò),這并不影響所有包含 promise 正常的拒絕操作。與 Promise.all() 類似,合成的 promise 會(huì)靜默處理所有包含 promise 的拒絕操作。

let p = Promise.race([
  Promise.reject(3),
  new Promise((res, rej) => setTimeout(rej, 1000))
]);
p.catch(reason => setTimeout(console.log, 0, reason)); // 3

// 沒(méi)有未處理的錯(cuò)誤

串行 promise 合成

這很像函數(shù)合成,即將多個(gè)函數(shù)合稱為一個(gè)函數(shù):

function addTwo(x) { return x + 2 }
function addThree(x) { return x + 3 }
function addFive(x) { return x + 5 }
function addTen(x) {
  return addFive(addTwo(addThree(x)));
}
console.log(addTen(7)); // 17

類似地,promise 也可以像這樣合成起來(lái),漸進(jìn)地消費(fèi)一個(gè)值,并返回一個(gè)結(jié)果:

function addTen(x) {
  return Promise.resolve(x)
    .then(addTwo)
    .then(addThree)
    .then(addFive);
}
addTen(8).then(console.log); // 18

利用 Array.prototype.reduce 可以更簡(jiǎn)潔,關(guān)于 reduce 的使用

function addTen(x) {
  return [addTwo, addThree, addFive]
    .reduce((promise, fn) => promise.then(fn), Promise.resolve(x));
}
addTen(8).then(console.log); // 18

Promise 擴(kuò)展

ES6 Promise 實(shí)現(xiàn)是很可靠,但也有不足之處,比如,很多第三方 promise 庫(kù)實(shí)現(xiàn)中都具備而 ES6 規(guī)范未涉及的兩個(gè)特性:取消和進(jìn)度追蹤。

Promise 取消

經(jīng)常會(huì)遇到 promise 正在處理過(guò)程中,程序卻不需要其結(jié)果的情形。這時(shí)候如果能夠取消 promise 就好了,某些第三方庫(kù)比如 Bluebird,就提供了這個(gè)特性。Kevin Smith 提到了“取消令牌”(cancel token),生成的令牌實(shí)例提供了一個(gè)接口,利用這個(gè)接口可以取消 promise;同時(shí)也提供了一個(gè) promise 實(shí)例,可以用來(lái)觸發(fā)取消后的操作并求值取消狀態(tài)。

class CancelToken {
  constructor(cancelFn) {
    this.promise = new Promise((res, rej) => {
      cancelFn(res);
    });
  }
}

這個(gè)類包裝了一個(gè) promise,把解決方法暴露給了一個(gè) cancelFn 參數(shù)。這樣,外部代碼就可以向構(gòu)造函數(shù)中傳入一個(gè)函數(shù),從而控制什么情況下可以取消 promise。這里的 promise 是令牌類的公共成員,因此可以給它添加處理程序以取消 promise

<button id="start">start</button>
<button id="cancel">cancel</button>

<script>
    class CancelToken {
        constructor(cancelFn) {
            this.promise = new Promise((res, rej) => {
                cancelFn(() => {
                    setTimeout(console.log, 0, '延遲取消');
                    res();
                });
            })
        }
    }

    const sBtn = document.querySelector('#start');
    const cBtn = document.querySelector('#cancel');

    function cancelable(delay) {
        setTimeout(console.log, 0, '設(shè)置延遲');
        return new Promise((res, rej) => {
            const id = setTimeout(() => {
                setTimeout(console.log, 0, '被延遲的解決');
                res();
            }, delay);
            
            const cancelToken = new CancelToken(cancelCb => cBtn.addEventListener('click', cancelCb));

            cancelToken.promise.then(() => clearTimeout(id));
        })
    }

    sBtn.addEventListener('click', () => cancelable(2000));
</script>

每次單擊“Start”按鈕都會(huì)開始計(jì)時(shí),并實(shí)例化一個(gè)新的 CancelToken 的實(shí)例。此時(shí),“Cancel” 按鈕一旦被點(diǎn)擊,就會(huì)觸發(fā)令牌實(shí)例中的期約解決。而解決之后,單擊“Start”按鈕設(shè)置的超時(shí)也會(huì)被取消。

Promise 進(jìn)度通知

執(zhí)行中的 promise 可能會(huì)有不少離散的“階段”,在最終解決之前必須一次經(jīng)過(guò)。某些情況下,監(jiān)控 promise 進(jìn)度會(huì)很有用。ES6 promise 不支持進(jìn)度追蹤,但可以通過(guò)擴(kuò)展來(lái)實(shí)現(xiàn)。

方式一:擴(kuò)展 Promise 類,為它添加 notify 方法

class TrackablePromise extends Promise {
  constructor(executor) {
    const notifyHandlers = [];
    super((res, rej) => {
      return executor(res, rej, (status) => {
        notifyHandlers.map(handler => handler(status));
      });
    });

    this.notifyHandlers = notifyHandlers;
  }

  notify(notifyHandler) {
    this.notifyHandlers.push(notifyHandlers);
    return this;
  }
}

這樣,TrackablePromise 就可以在執(zhí)行函數(shù)中使用 notify 函數(shù)了

let p = new TrackablePromise((res, rej, notify) => {
  function countdown(x) {
    if (x > 0) {
      notify(`${20 * x}% remaining`);
      setTimeout(() => countdown(x - 1), 1000);
    } else {
      res();
    }
  }

  countdown(5);
});

這個(gè) promise 會(huì)連續(xù) 5 次遞歸地設(shè)置 1000 毫秒的超時(shí),每個(gè)超時(shí)回調(diào)都會(huì)調(diào)用 notify 并傳入狀態(tài)值,假設(shè)通知處理程序簡(jiǎn)單地這樣寫:

p.notify((x) => setTimeout(console.log, 0, 'process:', x));

p.then(() => setTimeout(console.log, 0, 'completed'));

// (約 1 秒后)80% remaining 
// (約 2 秒后)60% remaining 
// (約 3 秒后)40% remaining 
// (約 4 秒后)20% remaining 
// (約 5 秒后)completed

notify 會(huì)返回 promise,所以可以連鎖調(diào)用,連續(xù)添加處理程序。多個(gè)處理程序會(huì)針對(duì)收到的每條消息分別執(zhí)行一遍,如下所示:

p.notify(x => setTimeout(console.log, 0, 'a:', x))
 .notify(x => setTimeout(console.log, 0, 'b:', x));

p.then(() => setTimeout(console.log, 0, 'completed'));

// (約 1 秒后) a: 80% remaining 
// (約 1 秒后) b: 80% remaining 
// (約 2 秒后) a: 60% remaining 
// (約 2 秒后) b: 60% remaining 
// (約 3 秒后) a: 40% remaining 
// (約 3 秒后) b: 40% remaining 
// (約 4 秒后) a: 20% remaining 
// (約 4 秒后) b: 20% remaining 
// (約 5 秒后) completed

總體來(lái)看,還是比較粗糙,但可以演示出如果使用通知報(bào)告進(jìn)度了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容