基礎(chǔ)版本
我們先以觀察者模式作為基石來搭建一個(gè)基礎(chǔ)版本,實(shí)現(xiàn)的功能如下:
function Promise(exector) { this.pending = []; this.value = undefined; const resolve = value => { if (this.pending) { this.value = value; for (const onFulfilled of this.pending) { // 通知觀察者。 onFulfilled(this.value); } this.pending = undefined; } }; exector(resolve);}Promise.prototype.then = function (onFulfilled) { if (this.pending) { // 還沒決議,先注冊(cè)觀察者。 this.pending.push(onFulfilled); } else { // 已決議,直接通知。 onFulfilled(this.value); }};// 測(cè)試一下。const p = new Promise(resolve => { setTimeout(() => resolve(666), 100);})p.then(res => console.log(‘res: %s’, res));// 輸出:// res: 666
代碼很簡單,應(yīng)該不用過多解釋,上面的完整代碼在這里:p0.js[2]。
這個(gè)基礎(chǔ)版本有個(gè)明顯的問題:then 不能進(jìn)行鏈?zhǔn)?/a>調(diào)用,接著就來優(yōu)化一下。
then鏈?zhǔn)秸{(diào)用
then 的鏈?zhǔn)秸{(diào)用會(huì)返回一個(gè)新的 Promise,并且 then 中回調(diào)的返回值會(huì)使這個(gè)新的 Promise 決議為“成功”狀態(tài)。
Promise.prototype.then = function (onFulfilled) { // “當(dāng)前”Promise,對(duì)于返回的新 Promise 而言,也是“前一個(gè)”Promise。 const prev = this; const promise = new Promise(resolve => { // 包裝 onFulfilled,使其可以“傳播”決議; // “前一個(gè)” Promise 決議后,決議返回的這個(gè)新 Promise。 const onSpreadFulfilled = function (value) { resolve(onFulfilled(value)); }; if (prev.pending) { prev.pending.push(onSpreadFulfilled); } else { onSpreadFulfilled(prev.value); } }); return promise;};// 測(cè)試一下。const p = new Promise(resolve => { setTimeout(() => resolve(666), 100);});p.then(res => { console.log(‘res1: %s’, res); return res + 1;).then(res => { console.log(‘res2: %s’, res);); // 輸出:// res1: 666// res2: 667
實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用的關(guān)鍵是如何決議返回的新 Promise?這里我對(duì)變量做了一些有含義的命名,方便理解:
上面的完整代碼在這里:p1.js[3]。
現(xiàn)在又有個(gè)新問題,如果 resolve 的 value 是個(gè) Promise,或者 onfulfilled 函數(shù)返回的結(jié)果是個(gè) Promise,那么鏈?zhǔn)絺鞑サ臎Q議值不應(yīng)該是這個(gè) Promise 本身,而是這個(gè) Promise 的決議值才對(duì),也就是要支持 Promise 的狀態(tài)傳遞。
狀態(tài)傳遞
在實(shí)現(xiàn)狀態(tài)傳遞之前,我們先來康康如何確定一個(gè)值是不是 Promise。我們可以用原型繼承來判斷:
return value instanceof Promise;
這樣的缺點(diǎn)是兼容性較差,你無法強(qiáng)制使用者的運(yùn)行環(huán)境上下文中只會(huì)用一種 Promise 的庫,或者在不同的運(yùn)行上下文中傳遞 Promise 實(shí)例。所以這里我們使用 鴨子類型[4] 來判斷 Promise,重點(diǎn)關(guān)注對(duì)象的行為,將 Promise 看作是一個(gè) thenable 對(duì)象。
function isPromise(value) { // 如果這個(gè)對(duì)象上可以調(diào)用 then 方法,就認(rèn)為它是一個(gè)“Promise”了。 return value && typeof value.then === ‘function’;}
接下來就來實(shí)現(xiàn)狀態(tài)傳遞了,實(shí)現(xiàn)的思路就是基于鴨子類型和“通知轉(zhuǎn)移”。我們先定義一個(gè)函數(shù):
function wrapToThenable(value) { if (isPromise(value)) { return value; } else { return { then: function (onFulfilled) { return wrapToThenable(onFulfilled(value)); } }; }}
顧名思義,這個(gè)函數(shù)的作用是用來把一個(gè)值包裝為 thenable 對(duì)象:如果 value 是 Promise 則直接返回;如果不是就包裝并返回一個(gè)有 then 方法的對(duì)象,也就是 thenable 對(duì)象。這個(gè) thenable 對(duì)象的作用是啥呢?接著看這里:
function Promise(exector) { this.pending = []; this.value = undefined; const resolve = value => { if (this.pending) { // 包裝為 thenable。 this.value = wrapToThenable(value); for (const onFulfilled of this.pending) { // 通知時(shí)改為調(diào)用 thenable 上的 then。 this.value.then(onFulfilled); } this.pending = undefined; } }; exector(resolve);}
resolve 決議時(shí),根據(jù) value 的類型不同,有兩種處理情況:
當(dāng)然 then 也要做一點(diǎn)修改:
Promise.prototype.then = function (onFulfilled) { const prev = this; const promise = new Promise(resolve => { const onSpreadFulfilled = function (value) { resolve(onFulfilled(value)); }; if (prev.pending) { prev.pending.push(onSpreadFulfilled); } else { // 這里也要改為調(diào)用 then。 prev.value.then(onSpreadFulfilled); } }); return promise;};// 測(cè)試一下。const p = new Promise(resolve => { setTimeout(() => resolve(666), 100);});p.then(res => { console.log(‘res1: %s’, res); return new Promise(resolve => { setTimeout(() => resolve(777), 100); });}).then(res => { console.log(‘res2: %s’, res);});// 輸出:// res1: 666// res2: 777
這里來總結(jié)一下狀態(tài)傳遞的設(shè)計(jì)思路。包裝為 thenable 對(duì)象非常關(guān)鍵,作用是保持了與 Promise 一致的行為,也就是接口一致。這樣在 resolve 時(shí)我們不用特定去判斷這個(gè)值是不是 Promise,而可以用統(tǒng)一的處理方式來通知觀察者;并且也順便完成了“通知轉(zhuǎn)移”,如果 value 還沒有決議,則 then 會(huì)注冊(cè)為回調(diào),如果已決議則 then 會(huì)立即執(zhí)行。
上面的完整代碼在這里:p2.js[5]。接下來,我們來完善一下 reject。
失敗狀態(tài)
當(dāng) Promise 決議失敗時(shí),then 方法里面將只執(zhí)行第二個(gè)參數(shù) onRejected 對(duì)應(yīng)的回調(diào)。首先我們需要另一個(gè)包裝函數(shù):
function wrapToRejected(value) { return { then: function (_, onRejected) { return wrapToThenable(onRejected(value)); } };}
這個(gè)函數(shù)的作用是一旦發(fā)生 reject(value) 時(shí),我們把 value 變?yōu)榱硪环N thenable 對(duì)象,這個(gè)對(duì)象在執(zhí)行 then 時(shí)只會(huì)調(diào)用 onRejected。
然后改變一下構(gòu)造函數(shù):
function Promise(exector) { // pending 變?yōu)橐粋€(gè)二維數(shù)組,里面存放的元素是 [onFulfilled, onRejected]。 this.pending = []; this.value = undefined; const resolve = value => { if (this.pending) { this.value = wrapToThenable(value); for (const handlers of this.pending) { this.value.then.apply(this.value, handlers); } this.pending = undefined; } }; const reject = value => { resolve(wrapToRejected(value)); }; exector(resolve, reject);}
現(xiàn)在有一個(gè)比較大的變化:this.pending 變?yōu)榱硕S數(shù)組。這樣 this.value.then.apply 在執(zhí)行時(shí)會(huì)有三種情況:
同樣 then 方法也要做一些修改:
Promise.prototype.then = function (onFulfilled, onRejected) { const prev = this; // 注意這里給了 onFulfilled、onRejected 默認(rèn)值。 onFulfilled = onFulfilled || function (value) { return value; }; onRejected = onRejected || function (value) { return wrapToRejected(value); }; const promise = new Promise(resolve => { const onSpreadFulfilled = function (value) { resolve(onFulfilled(value)); }; const onSpreadRejected = function (value) { resolve(onRejected(value)); }; if (prev.pending) { prev.pending.push([onSpreadFulfilled, onSpreadRejected]); } else { prev.value.then(onSpreadFulfilled, onSpreadRejected); } }); return promise;};// 測(cè)試一下。const p = new Promise((resolve, reject) => { setTimeout(() => reject(666), 100);});p.then(undefined, err => { console.log(‘err1: %s’, err); return 1;}).then(res => { console.log(‘res1: %s’, res);});// 輸出:// err1: 666// res1: 1
我們要特別注意一下增加了 onFulfilled、onRejected 的默認(rèn)值。在實(shí)際使用 then 時(shí),可能只會(huì)專注處理成功或者失敗的回調(diào),但是我們又需要另外一種狀態(tài)要繼續(xù)傳播下去。這里可能有點(diǎn)不好理解,可以代入數(shù)據(jù)模擬一下。上面的完整代碼在這里:p3.js[6]。
又到了思考總結(jié)時(shí)間,thenable 這個(gè)接口是關(guān)鍵所在。通過兩個(gè)包裝對(duì)象,分別處理成功和失敗的狀態(tài),在通知觀察者時(shí)可以保持統(tǒng)一的邏輯,這個(gè)設(shè)計(jì)是不是感覺很妙呢?
接下來我們要處理一下調(diào)用時(shí)會(huì)產(chǎn)生異常的問題。
異常處理
我們先思考一下會(huì)有哪些地方會(huì)產(chǎn)生異常?第一個(gè)是構(gòu)造函數(shù)里面 exector 執(zhí)行的時(shí)候:
function Promise(exector) { this.pending = []; this.value = undefined; const resolve = value => { // … }; const reject = value => { resolve(wrapToRejected(value)); }; try { exector(resolve, reject); } catch (e) { // 如果有異常產(chǎn)生,狀態(tài)變?yōu)椤笆 薄? reject(e); }}
然后是onFulfilled 和 onRejected 執(zhí)行的時(shí)候。當(dāng)在以上兩個(gè)方法里產(chǎn)生異常時(shí),狀態(tài)要變?yōu)槭?,并且需要把異常傳播下去。then 的改動(dòng)如下:
Promise.prototype.then = function (onFulfilled, onRejected) { // … // 產(chǎn)生異常的時(shí)候包裝一下。 const errHandler = returnWhenError(err => wrapToRejected(err)); onFulfilled = errHandler(onFulfilled); onRejected = errHandler(onRejected); const promise = new Promise(resolve => { const onSpreadFulfilled = function (value) { resolve(onFulfilled(value)); }; const onSpreadRejected = function (value) { resolve(onRejected(value)); }; if (prev.pending) { prev.pending.push([onSpreadFulfilled, onSpreadRejected]); } else { prev.value.then(onSpreadFulfilled, onSpreadRejected); } }); return promise;};// 封裝為一個(gè)可重用的高階函數(shù)。// 如果 fun 執(zhí)行失敗了,則返回 onError 的結(jié)果。function returnWhenError(onError) { return fun => (…args) => { let result; try { result = fun(…args); } catch (e) { result = onError(e); } return result; };}
然后我們可以加入 catch 方法:
Promise.prototype.catch = function (onRejected) { // 在 then 中忽略掉“成功”狀態(tài)的回調(diào)。 return Promise.prototype.then.call(this, undefined, onRejected);};// 測(cè)試一下。const p = new Promise(resolve => { setTimeout(() => resolve(666), 100);});p.then(res => { console.log(‘res1: %s’, res); throw new Error(‘test error1’);}).then(undefined, err => { console.log(‘err1: %s’, err.message); throw new Error(‘test error2’);}).catch(err => { console.log(‘err2: %s’, err.message);});// 輸出:// res1: 666// err1: test error1// err2: test error2
上面的完整代碼在這里:p4.js[7]。
到了這里,基本上 Promise 的基本功能就差不多完成了。不過還有一些不太完善的地方,我們來繼續(xù)做一些優(yōu)化。
一些優(yōu)化
封裝私有變量
this.pending 和 this.value 從外部是可以讀寫的,不夠安全和健壯。而我又還是想用構(gòu)造函數(shù)和原型方法,不想用閉包來封裝。我這里采用的是 WeakMap[8] 來達(dá)到目的,關(guān)鍵的修改如下:
const refMap = new WeakMap();// …function Promise(exector) { // 用當(dāng)前的實(shí)例引用作為 key,把想隱藏的數(shù)據(jù)放進(jìn)一個(gè)對(duì)象里。 refMap.set(this, { pending: [], value: undefined }); const resolve = value => { // 取出封裝的數(shù)據(jù)。 const data = refMap.get(this); if (data.pending) { data.value = wrapToThenable(value); for (const handlers of data.pending) { data.value.then.apply(data.value, handlers); } data.pending = undefined; } }; // …}
同樣 then 也修改一下:
Promise.prototype.then = function (onFulfilled, onRejected) { // … const promise = new Promise(resolve => { const onSpreadFulfilled = function (value) { resolve(onFulfilled(value)); }; const onSpreadRejected = function (value) { resolve(onRejected(value)); }; // 取出封裝的數(shù)據(jù)。 const data = refMap.get(prev); if (data.pending) { data.pending.push([onSpreadFulfilled, onSpreadRejected]); } else { data.value.then(onSpreadFulfilled, onSpreadRejected); } }); return promise;};
上面的完整代碼在這里:p5.js[9]。
當(dāng) Promise 實(shí)例被垃圾回收時(shí),對(duì)應(yīng)在 WeakMap 中的私有數(shù)據(jù)對(duì)象引用也會(huì)被消除,沒有內(nèi)存泄漏問題,這種方案非常適合用來封裝私有變量。
調(diào)用順序
目前的 Promise 在執(zhí)行時(shí)有調(diào)用順序問題,比如:
const p = new Promise(resolve => resolve(1));p.then(res => { console.log(‘res1:’, res); return res + 1;}).then(res => { console.log(‘res2:’, res);});p.then(res => { console.log(‘res3:’, res);});console.log(‘Hi!’);// 目前的輸出是:// res1: 1// res2: 2// res3: 1// Hi!// 正確的輸出應(yīng)該是:// Hi!// res1: 1// res3: 1// res2: 2
一個(gè)簡單的做法是利用 setTimeout 來改進(jìn):
function Promise(exector) { // … const resolve = value => { const data = refMap.get(this); if (data.pending) { data.value = wrapToThenable(value); for (const handlers of data.pending) { // 延遲執(zhí)行。 enqueue(() => { data.value.then.apply(data.value, handlers); }); } data.pending = undefined; } }; // …}Promise.prototype.then = function (onFulfilled, onRejected) { // … const promise = new Promise(resolve => { // … if (data.pending) { data.pending.push([onSpreadFulfilled, onSpreadRejected]); } else { // 延遲執(zhí)行。 enqueue(() => { data.value.then(onSpreadFulfilled, onSpreadRejected); }); } }); return promise;};function enqueue(callback) { setTimeout(callback, 1);}
enqueue 的作用是模擬按入隊(duì)順序來延遲執(zhí)行函數(shù)。通過對(duì)所有 then 調(diào)用的延遲執(zhí)行,可以保證按正確的注冊(cè)順序和決議順序來執(zhí)行了,上面的完整代碼在這里:p6.js[10]。