本篇來說講如何模擬實現一個 Promise 的基本功能,網上這類文章已經不少,本篇筆墨會比較多,由於想用本身的理解,用白話文來說講git
Promise 的基本規範,參考了這篇:【翻譯】Promises/A+規範github
但說實話,太多的專業術語,以及基本按照標準規範格式翻譯而來,有些內容,若是不是對規範的閱讀方式比較熟悉的話,那是很難理解這句話的內容的面試
我就是屬於沒直接閱讀過官方規範的,因此即便在看中文譯版時,有些表達仍舊須要花費不少時間去理解,基於此,纔想要寫這篇typescript
Promise 是一種異步編程方案,經過 then 方法來註冊回調函數,經過構造函數參數來控制異步狀態編程
Promise 的狀態變化有兩種,成功或失敗,狀態一旦變動結束,就不會再改變,後續全部註冊的回調都能接收此狀態,同時異步執行結果會經過參數傳遞給回調函數promise
var p = new Promise((resolve, reject) => { // do something async job // resolve(data); // 任務結束,觸發狀態變化,通知成功回調的處理,並傳遞結果數據 // reject(err); // 任務異常,觸發狀態變化,通知失敗回調的處理,並傳遞失敗緣由 }).then(value => console.log(value)) .catch(err => console.error(err)); p.then(v => console.log(v), err => console.error(err));
上述例子是基本用法,then 方法返回一個新的 Promise,因此支持鏈式調用,可用於一個任務依賴於上一個任務的執行結果這種場景瀏覽器
對於同一個 Promise 也能夠調用屢次 then 來註冊多個回調處理緩存
經過使用來理解它的功能,清楚它都支持哪些功能後,咱們在模擬實現時,才能知道到底須要寫些什麼代碼異步
因此,這裏來比較細節的羅列下 Promise 的基本功能:async
then(null, onRejected)
的語法糖new Promise(task)
時,傳入的 task 函數就會立刻被執行了,但傳給 then 的回調函數,會做爲微任務放入隊列中等待執行(通俗理解,就是下降優先級,延遲執行,不知道怎麼模擬微任務的話,可使用 setTimeout 生成的宏任務來模擬)這些基本功能就足夠 Promise 的平常使用了,因此咱們的模擬實現版的目標就是實現這些功能
Promise 的基本功能清楚了,那咱們代碼該怎麼寫,寫什麼?
從代碼角度來看的話,無非也就是一些變量、函數,因此,咱們就能夠來針對各個功能點,思考下,都須要哪些代碼:
task 處理函數和註冊的回調處理函數都是使用者在使用 Promise 時,自行根據業務須要編寫的代碼
那麼,剩下的也就是咱們在實現 Promise 時須要編寫的代碼了,這樣一來,Promise 的骨架其實也就能夠出來了:
export type statusChangeFn = (value?: any) => void; /* 回調函數類型 */ export type callbackFn = (value?: any) => any; export class Promise { /* 三種狀態 */ private readonly PENDING: string = 'pending'; private readonly RESOLVED: string = 'resolved'; private readonly REJECTED: string = 'rejected'; /* promise當前狀態 */ private _status: string; /* promise執行結果 */ private _value: string; /* 成功的回調 */ private _resolvedCallback: Function[] = []; /* 失敗的回調 */ private _rejectedCallback: Function[] = []; /** * 處理 resolve 的狀態變動相關工做,參數接收外部傳入的執行結果 */ private _handleResolve(value?: any) {} /** * 處理 reject 的狀態變動相關工做,參數接收外部傳入的失敗緣由 */ private _handleReject(value?: any) {} /** * 構造函數,接收一個 task 處理函數,task 有兩個可選參數,類型也是函數,其實也就是上面的兩個處理狀態變動工做的函數(_handleResolve,_handleReject),用來給使用者來觸發狀態變動使用 */ constructor(task: (resolve?: statusChangeFn, reject?: statusChangeFn) => void) {} /** * then 方法,接收兩個可選參數,用於註冊成功或失敗時的回調處理,因此類型也是函數,函數有一個參數,接收 Promise 執行結果或失敗緣由,同時可返回任意值,做爲新 Promise 的執行結果 */ then(onResolved?: callbackFn, onRejected?: callbackFn): Promise { return null; } catch(onRejected?: callbackFn): Promise { return this.then(null, onRejected); } }
注意:骨架這裏的代碼,我用了 TypeScript,這是一種強類型語言,能夠標明各個變量、參數類型,便於講述和理解,看不懂不要緊,下面有編譯成 js 版的
因此,咱們要補充完成的其實就是三部分:Promise 構造函數都作了哪些事、狀態變動須要作什麼處理、then 註冊回調函數時須要作的處理
Promise 的構造函數作的事,其實很簡單,就是立刻執行傳入的 task 處理函數,並將本身內部提供的兩個狀態變動處理的函數傳遞給 task,同時將當前 promise 狀態置爲 PENDING(執行中)
constructor(task) { // 1. 將當前狀態置爲 PENDING this._status = this.PENDING; // 參數類型校驗 if (!(task instanceof Function)) { throw new TypeError(`${task} is not a function`); } try { // 2. 調用 task 處理函數,並將狀態變動通知的函數傳遞過去,須要注意 this 的處理 task(this._handleResolve.bind(this), this._handleReject.bind(this)); } catch (e) { // 3. 若是 task 處理函數發生異常,當作失敗來處理 this._handleReject(e); } }
Promise 狀態變動的相關處理是我以爲實現 Promise 最難的一部分,這裏說的難並非說代碼有多複雜,而是說這塊須要理解透,或者看懂規範並不大容易,由於須要考慮一些處理,網上看了些 Promise 實現的文章,這部分都存在問題
狀態變動的工做,是由傳給 task 處理函數的兩個函數參數被調用時觸發進行,如:
new Promise((resolve, reject) => { resolve(1); });
resolve 或 reject 的調用,就會觸發 Promise 內部去處理狀態變動的相關工做,還記得構造函數作的事吧,這裏的 resolve 或 reject 其實就是對應着內部的 _handleResolve 和 _handleReject 這兩個處理狀態變動工做的函數
但這裏有一點須要注意,是否是 resolve 一調用,Promise 的狀態就必定發生變化了呢?
答案不是的,網上看了些這類文章,他們的處理是 resolve 調用,狀態就變化,就去處理回調隊列了
但實際上,這樣是錯的
狀態的變動,其實依賴於 resolve 調用時,傳遞過去的參數的類型,由於這裏能夠傳遞任意類型的值,能夠是基本類型,也能夠是 Promise
當類型不同時,對於狀態的變動處理是不同的,開頭那篇規範裏面有詳細的說明,但要看懂並不大容易,我這裏就簡單用個人理解來說講:
x.then(this._handleResolve, this._handleReject)
x.then(this._handleResolve, this._handleReject)
因此你能夠看到,其實 resolve 即便調用了,但內部並不必定就會發生狀態變化,只有當 resolve 傳遞的參數類型既不是 Promise 對象類型,也不是具備 then 方法的 thenable 對象時,狀態纔會發生變化
而當傳遞的參數是 Promise 或具備 then 方法的 thenable 對象時,差很少又是至關於遞歸回到第一步的等待 task 函數的處理了
想一想爲何須要這種處理,或者說,爲何須要這麼設計?
這是由於,存在這樣一種場景:有多個異步任務,這些異步任務之間是同步關係,一個任務的執行依賴於上一個異步任務的執行結果,當這些異步任務經過 then 的鏈式調用組合起來時,then 方法產生的新的 Promise 的狀態變動是依賴於回調函數的返回值。因此這個狀態變動須要支持當值類型是 Promise 時的異步等待處理,這條異步任務鏈才能獲得預期的執行效果
當大家去看規範,或看規範的中文版翻譯,其實有關於這個的更詳細處理說明,好比開頭給的連接的那篇文章裏有專門一個模塊:Promise 的解決過程,也表示成 [[Resolve]](promise, x)
就是在講這個
但我想用本身的理解來描述,這樣比較容易理解,雖然我也只能描述個大概的工做,更細節、更全面的處理應該要跟着規範來,下面就看看代碼:
/** * resolve 的狀態變動處理 */ _handleResolve(value) { if (this._status === this.PENDING) { // 1. 若是 value 是 Promise,那麼等待 Promise 狀態結果出來後,再從新作狀態變動處理 if (value instanceof Promise) { try { // 這裏之因此不須要用 bind 來注意 this 問題是由於使用了箭頭函數 // 這裏也能夠寫成 value.then(this._handleResole.bind(this), this._handleReject.bind(this)) value.then(v => { this._handleResolve(v); }, err => { this._handleReject(err); }); } catch(e) { this._handleReject(e); } } else if (value && value.then instanceof Function) { // 2. 若是 value 是具備 then 方法的對象時,那麼將這個 then 方法當作 task 處理函數,把狀態變動的觸發工做交由 then 來處理,注意 this 的處理 try { const then = value.then; then.call(value, this._handleResolve.bind(this), this._handleReject.bind(this)); } catch(e) { this._handleReject(e); } } else { // 3. 其餘類型,狀態變動、觸發成功的回調 this._status = this.RESOLVED; this._value = value; setTimeout(() = { this._resolvedCallback.forEach(callback => { callback(); }); }); } } } /** * reject 的狀態變動處理 */ _handleReject(value) { if (this._status === this.PENDING) { this._status = this.REJECTED; this._value = value; setTimeout(() => { this._rejectedCallback.forEach(callback => { callback(); }); }); } }
then 方法負責的職能其實也很複雜,既要返回一個新的 Promise,這個新的 Promise 的狀態和結果又要依賴於回調函數的返回值,而回調函數的執行又要看狀況是緩存進回調函數隊列裏,仍是直接取依賴的 Promise 的狀態結果後,丟到微任務隊列裏去執行
雖然職能複雜是複雜了點,但其實,實現上,都是依賴於前面已經寫好的構造函數和狀態變動函數,因此只要前面幾個步驟實現上沒問題,then 方法也就不會有太大的問題,直接看代碼:
/** * then 方法,接收兩個可選參數,用於註冊回調處理,因此類型也是函數,且有一個參數,接收 Promise 執行結果,同時可返回任意值,做爲新 Promise 的執行結果 */ then(onResolved, onRejected) { // then 方法返回一個新的 Promise,新 Promise 的狀態結果依賴於回調函數的返回值 return new Promise((resolve, reject) => { // 對回調函數進行一層封裝,主要是由於回調函數的執行結果會影響到返回的新 Promise 的狀態和結果 const _onResolved = () => { // 根據回調函數的返回值,決定如何處理狀態變動 if (onResolved && onResolved instanceof Function) { try { const result = onResolved(this._value); resolve(result); } catch(e) { reject(e); } } else { // 若是傳入非函數類型,則將上個Promise結果傳遞給下個處理 resolve(this._value); } }; const _onRejected = () => { if (onRejected && onRejected instanceof Function) { try { const result = onRejected(this._value); resolve(result); } catch(e) { reject(e); } } else { reject(this._value); } }; // 若是當前 Promise 狀態還沒變動,則將回調函數放入隊列裏等待執行 // 不然直接建立微任務來處理這些回調函數 if (this._status === this.PENDING) { this._resolvedCallback.push(_onResolved); this._rejectedCallback.push(_onRejected); } else if (this._status === this.RESOLVED) { setTimeout(_onResolved); } else if (this._status === this.REJECTED) { setTimeout(_onRejected); } }); }
由於目的在於理清 Promise 的主要功能職責,因此個人實現版並無按照規範一步步來,細節上,或者某些特殊場景的處理,可能欠缺考慮
好比對各個函數參數類型的校驗處理,由於 Promise 的參數基本都是函數類型,但即便傳其餘類型,也仍舊不影響 Promise 的使用
好比爲了不被更改實現,一些內部變量能夠改用 Symbol 實現
但大致上,考慮了上面這些步驟實現,基本功能也差很少了,重要的是狀態變動這個的處理要考慮全一點,網上一些文章的實現版,這個是漏掉考慮的
還有當面試遇到讓你手寫實現 Promise 時不要慌,能夠按着這篇的思路,先把 Promise 的基本用法回顧一下,而後回想一下它支持的功能,再而後內心有個大概的骨架,其實無非也就是幾個內部變量、構造函數、狀態變動函數、then 函數這幾塊而已,但死記硬背並很差,有個思路,一步步來,總能回想起來
源碼補上了 catch,resolve 等其餘方法的實現,這些其實都是基於 Promise 基本功能上的一層封裝,方便使用
class Promise { /** * 構造函數負責接收並執行一個 task 處理函數,並將本身內部提供的兩個狀態變動處理的函數傳遞給 task,同時將當前 promise 狀態置爲 PENDING(執行中) */ constructor(task) { /* 三種狀態 */ this.PENDING = 'pending'; this.RESOLVED = 'resolved'; this.REJECTED = 'rejected'; /* 成功的回調 */ this._resolvedCallback = []; /* 失敗的回調 */ this._rejectedCallback = []; // 1. 將當前狀態置爲 PENDING this._status = this.PENDING; // 參數類型校驗 if (!(task instanceof Function)) { throw new TypeError(`${task} is not a function`); } try { // 2. 調用 task 處理函數,並將狀態變動通知的函數傳遞過去,須要注意 this 的處理 task(this._handleResolve.bind(this), this._handleReject.bind(this)); } catch (e) { // 3. 若是 task 處理函數發生異常,當作失敗來處理 this._handleReject(e); } } /** * resolve 的狀態變動處理 */ _handleResolve(value) { if (this._status === this.PENDING) { if (value instanceof Promise) { // 1. 若是 value 是 Promise,那麼等待 Promise 狀態結果出來後,再從新作狀態變動處理 try { // 這裏之因此不須要用 bind 來注意 this 問題是由於使用了箭頭函數 // 這裏也能夠寫成 value.then(this._handleResole.bind(this), this._handleReject.bind(this)) value.then(v => { this._handleResolve(v); }, err => { this._handleReject(err); }); } catch(e) { this._handleReject(e); } } else if (value && value.then instanceof Function) { // 2. 若是 value 是具備 then 方法的對象時,那麼將這個 then 方法當作 task 處理函數,把狀態變動的觸發工做交由 then 來處理,注意 this 的處理 try { const then = value.then; then.call(value, this._handleResolve.bind(this), this._handleReject.bind(this)); } catch(e) { this._handleReject(e); } } else { // 3. 其餘類型,狀態變動、觸發成功的回調 this._status = this.RESOLVED; this._value = value; setTimeout(() => { this._resolvedCallback.forEach(callback => { callback(); }); }); } } } /** * reject 的狀態變動處理 */ _handleReject(value) { if (this._status === this.PENDING) { this._status = this.REJECTED; this._value = value; setTimeout(() => { this._rejectedCallback.forEach(callback => { callback(); }); }); } } /** * then 方法,接收兩個可選參數,用於註冊回調處理,因此類型也是函數,且有一個參數,接收 Promise 執行結果,同時可返回任意值,做爲新 Promise 的執行結果 */ then(onResolved, onRejected) { // then 方法返回一個新的 Promise,新 Promise 的狀態結果依賴於回調函數的返回值 return new Promise((resolve, reject) => { // 對回調函數進行一層封裝,主要是由於回調函數的執行結果會影響到返回的新 Promise 的狀態和結果 const _onResolved = () => { // 根據回調函數的返回值,決定如何處理狀態變動 if (onResolved && onResolved instanceof Function) { try { const result = onResolved(this._value); resolve(result); } catch(e) { reject(e); } } else { // 若是傳入非函數類型,則將上個Promise結果傳遞給下個處理 resolve(this._value); } }; const _onRejected = () => { if (onRejected && onRejected instanceof Function) { try { const result = onRejected(this._value); resolve(result); } catch(e) { reject(e); } } else { reject(this._value); } }; // 若是當前 Promise 狀態還沒變動,則將回調函數放入隊列裏等待執行 // 不然直接建立微任務來處理這些回調函數 if (this._status === this.PENDING) { this._resolvedCallback.push(_onResolved); this._rejectedCallback.push(_onRejected); } else if (this._status === this.RESOLVED) { setTimeout(_onResolved); } else if (this._status === this.REJECTED) { setTimeout(_onRejected); } }); } catch(onRejected) { return this.then(null, onRejected); } static resolve(value) { if (value instanceof Promise) { return value; } return new Promise((reso) => { reso(value); }); } static reject(value) { if (value instanceof Promise) { return value; } return new Promise((reso, reje) => { reje(value); }); } }
網上有一些專門測試 Promise 的庫,能夠直接藉助這些,好比:promises-tests
我這裏就舉一些基本功能的測試用例:
// 測試鏈式調用 new Promise(r => { console.log('0.--同步-----'); r(); }).then(v => console.log('1.-----------------')) .then(v => console.log('2.-----------------')) .then(v => console.log('3.-----------------')) .then(v => console.log('4.-----------------')) .then(v => console.log('5.-----------------')) .then(v => console.log('6.-----------------')) .then(v => console.log('7.-----------------'))
0.--同步----- 1.----------------- 2.----------------- 3.----------------- 4.----------------- 5.----------------- 6.----------------- 7.-----------------
// 測試屢次調用 then 註冊多個回調處理 var p = new Promise(r => r(1)); p.then(v => console.log('1-----', v), err => console.error('error', err)); p.then(v => console.log('2-----', v), err => console.error('error', err)); p.then(v => console.log('3-----', v), err => console.error('error', err)); p.then(v => console.log('4-----', v), err => console.error('error', err));
1----- 1 2----- 1 3----- 1 4----- 1
// 測試異步場景 new Promise(r => { r(new Promise(a => setTimeout(a, 5000)).then(v => 1)); }) .then(v => { console.log(v); return new Promise(a => setTimeout(a, 1000)).then(v => 2); }) .then(v => console.log('success', v), err => console.error('error', err));
1 // 5s 後才輸出 success 2 // 再2s後才輸出
這個測試,能夠檢測出 resolve 的狀態變動到底有沒有根據規範,區分不一樣場景進行不一樣處理,你能夠網上隨便找一篇 Promise 的實現,把它的代碼貼到瀏覽器的 console 裏,而後測試一下看看,就知道有沒有問題了
// 測試執行結果類型爲 Promise 對象場景(Promise 狀態 5s 後變化) new Promise(r => { r(new Promise(a => setTimeout(a, 5000))); }).then(v => console.log('success', v), err => console.error('error', err));
success undefined // 5s 後才輸出
// 測試執行結果類型爲 Promise 對象場景(Promise 狀態不會發生變化) new Promise(r => { r(new Promise(a => 1)); }).then(v => console.log('success', v), err => console.error('error', err));
// 永遠都不輸出
// 測試執行結果類型爲具備 then 方法的 thenable 對象場景(then 方法內部會調用傳遞的函數參數) new Promise(r => { r({ then: (a, b) => { return a(1); } }); }).then(v => console.log('success', v), err => console.error('error', err));
success 1
// // 測試執行結果類型爲具備 then 方法的 thenable 對象場景(then 方法內部不會調用傳遞的函數參數) new Promise(r => { r({ then: (a, b) => { return 1; } }); }).then(v => console.log('success', v), err => console.error('error', err));
// 永遠都不輸出
// 測試執行結果類型爲具備 then 的屬性,但屬性值類型非函數 new Promise(r => { r({ then: 111 }); }).then(v => console.log('success', v), err => console.error('error', err));
success {then: 111}
// 測試當 Promise rejectd 時,reject 的狀態結果會一直傳遞到能夠處理這個失敗結果的那個 then 的回調中 new Promise((r, j) => { j(1); }).then(v => console.log('success', v)) .then(v => console.log('success', v), err => console.error('error', err)) .catch(err => console.log('catch', err));
error 1
// 測試傳給 then 的參數是非函數類型時,執行結果和狀態會一直傳遞 new Promise(r => { r(1); }).then(1) .then(null, err => console.error('error', err)) .then(v => console.log('success', v), err => console.error('error', err));
success 1
// 測試 rejectd 失敗被處理後,就不會繼續傳遞 rejectd new Promise((r,j) => { j(1); }).then(2) .then(v => console.log('success', v), err => console.error('error', err)) .then(v => console.log('success', v), err => console.error('error', err));
error 1 success undefined
最後,當你本身寫完個模擬實現 Promise 時,你能夠將代碼貼到瀏覽器上,而後本身測試下這些用例,跟官方的 Promise 執行結果比對下,你就能夠知道,你實現的 Promise 基本功能上有沒有問題了
固然,須要更全面的測試的話,仍是得藉助一些測試庫
不過,本身實現一個 Promise 的目的其實也就在於理清 Promise 基本功能、行爲、原理,因此這些用例能測經過的話,那麼基本上也就掌握這些知識點了