大家好,歡迎來到前端研習圈的今日分享。
前言
本系列上期帶著大家一起拆解了 Promises/A+ 規範。從概念,術語,約束條例
等方面瞭解了規範
那麼本期我們要做的就是從規範到實現,並且透過官方的所有測試用例。為了和原生的 Promise 有所區別,我們就把這一版實現命名為 _Promise
。完全形態已經上傳到 github,需要的同學自取
提示:_Promise 僅關注具體實現,不關注成員方法具體應該是私有還是公有等設計細節
原始碼地址 ->
https://github.com/Mumujianguang/_promise
接下來我們就進入主題,首先我們大概整理一下 todo list
- 定義 Promise 的狀態列舉
- 定義 Promise 的類結構
- 實現建構函式
- 實現 then 方法
結構設計
首先,我們先定義一個列舉物件,將 Promise 的三種狀態定義出來
const PromiseState = {
pending: 'pending',
fulfilled: 'fulfilled',
rejected: 'rejected'
}
然後簡單設計一下類結構,同時將 state 初始化為 pending 狀態
class _Promise {
state = PromiseState.pending;
value;
reason;
fulfilledQueue = [];
rejectedQueue = [];
constructor(executor) {}
resolve(value) {}
reject(reason) {}
then(onFulfilled, onRejected) {}
}
因為 then 可以呼叫多次,所以我們設計 fulfilledQueue
和 rejectedQueue
兩個陣列來分別儲存 then 所註冊的 成功的回撥 和 失敗的回撥
為了後續方便內部呼叫,這裡將 resolve 和 reject 兩個方法也直接定義在類上
實現
有了基礎結構,那麼我們就可以開始著手實現各個方法了。先從 constructor
開始
constructor
回顧一下 Promise
的用法,為了方便講解,我們把在構造 Promise
例項時傳入的函式單獨提出來,它的學名叫 executor,接收 resolve
和 reject
兩個引數
const executor = (resolve, reject) => {}
const p = new Promise(executor)
看到這兒,相信大家應該都有 constructor
的實現思路了。
但需要注意一點,為了防止 executor 執行時內部報錯,需要 try catch 處理一下,並且在 catch 的場景直接將 Promise
reject
掉
constructor(executor) {
try {
executor(
(value) => this.resolve(value),
(reason) => this.reject(reason)
)
} catch(e) {
this.reject(e);
}
}
以上就是 constructor
的實現邏輯,接下來我們順著這個思路繼續
resolve & reject
在執行 executor 時,我們將 resolve
和 reject
作為引數傳了進去,我們一起回顧一下它們的作用是什麼
- 接收一個 value/reason
- 將
Promise
的狀態修改為成功/失敗 - 把 value/reason 作為 成功/失敗 回撥的引數並按照註冊的順序批次執行
- 狀態一旦改變就不能再被呼叫
功能點很清晰,那麼我們就可以一條一條去實現它們,直接看程式碼吧
resolve(value) {
if (this.state !== PromiseState.pending) {
return;
}
this.state = PromiseState.fulfilled;
this.value = value;
this.fulfilledQueue.forEach((onFulfilled) => onFulfilled(value))
}
reject(reason) {
if (this.state !== PromiseState.pending) {
return;
}
this.state = PromiseState.rejected;
this.reason = reason;
this.rejectedQueue.forEach((onRejected) => onRejected(reason))
}
到這裡,我們就完成了 resolve & reject
的實現了,還是比較簡單對不對,那麼接下來要上強度咯
then
首先我們思考一下 then
的作用是什麼,再同步回顧一下用法
const p = new Promise(resolve => resolve('done'))
p.then(
value => console.log(value),
reason => console.log(reason),
)
then
的功能其實很簡單,就是單純註冊 成功/失敗 的回撥,但就是看似如此簡單的方法,它的實現難度卻是整個 Promise
中最高的。
但不要慌,我們今天的目標就是要搞懂它,翻過那座山(背後還是山)!
結合上期規範中所講,我們先簡單概括兩點
then
返回的是一個新的Promise
- 接收 onFulfilled 和 onRejected 作為引數
先寫出如下程式碼
then(onFulfilled, onRejected) {
const p2 = new _Promise((resolve, reject) => {
// TODO
})
return p2;
}
現在 then 的整體框架有了,我們繼續思考,由於 onFulfilled 和 onRejected 的返回值會決定 p2
的狀態,那麼在註冊之前,我們肯定需要對這兩個方法做一層包裝
,將新的Promise的 resolve 和 reject 的執行權
與 onFulfilled/onRejected 的返回值關聯起來,由返回值的具體情況決定
也就是說 then
的其餘邏輯我們需要寫在 新Promise 的 executor 中。同時還需要注意的是,在 executor 中我們就需要訪問 p2
,但 p2 是在 executor 執行完畢之後才被賦的值,直接訪問肯定會報錯
then(onFulfilled, onRejected) {
const p2 = new _Promise((resolve, reject) => {
console.log(p2) // Uncaught ReferenceError: Cannot access 'p2' before initialization
})
return p2;
}
Uncaught ReferenceError: Cannot access 'p2' before initialization
如上程式碼所示,我們會得到一個與預期一致的報錯,怎麼規避呢?其實很容易解決,放到非同步任務裡面去執行不就好了嗎,這裡我們用 queueMicrotask
來實現
then(onFulfilled, onRejected) {
const p2 = new _Promise((resolve, reject) => {
queueMicrotask(() => {
console.log(p2) // _Promise {}
})
})
return p2;
}
搞定,現在就能正常訪問 p2
了
那麼接下我們繼續按照規範的約束條例給 then
的實現添磚加瓦,先梳理一下大致要做的事情
- 給 onFulfilled 和 onRejected 新增相容邏輯(參考條例 2.2.1 & 2.2.7.3 & 2.2.7.4)
- 包裝 onFulfilled 和 onRejected,它們的返回值將決定
p2
的 resolve 和 reject 如何執行。因此這裡我們再抽象一層 包裝器 函式(wrapCallback
)出來,由這個函式來返回包裝後的 onFulfilled(resolveCallback
) 和 onRejected(rejectCallback
) - 判斷當前
Promise
的狀態是否已經確定,是的話則直接呼叫resolveCallback
或rejectCallback
,否則將它們註冊到各自的回撥佇列中
梳理完畢,就敲程式碼實現吧
then(onFulfilled, onRejected) {
const p2 = new _Promise((resolve, reject) => {
queueMicrotask(() => {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => resolve(this.value);
onRejected = typeof onRejected === 'function' ? onRejected : () => reject(this.reason);
const resolveCallback = this.wrapCallback(
p2,
onFulfilled,
resolve,
reject
);
const rejectCallback = this.wrapCallback(
p2,
onRejected,
resolve,
reject
);
if (this.state === PromiseState.fulfilled) {
resolveCallback(this.value);
return;
}
if (this.state === PromiseState.rejected) {
rejectCallback(this.reason);
return;
}
this.fulfilledQueue.push(
resolveCallback
)
this.rejectedQueue.push(
rejectCallback
)
})
})
return p2;
}
至此 then
方法我們就已經實現啦,完結撒花!最難的部分也不過如此...
等等,不太對勁,then
是實現完了,但我們剛剛又引入了一層包裝器
還沒實現呢,還得繼續呀各位~
順便提醒一下大家,還記得上期我們留的一個大坑嗎,沒錯就是規範中的約束條例2.3
所描述的 Promise Resolution Procedure 流程,但我們現在不是隻剩下 wrapCallback
了嗎,所以這個處理流程只能在 wrapCallback
中實現了,為了和規範保持一致,我們把 Promise Resolution Procedure 流程單獨用一個方法來實現,就取名叫 resolutionProcedure
吧
這樣一來 wrapCallback
的實現就異常簡單了,但注意 then
的回撥需要放在 micro task 中去執行,這裡我們還是用 queueMicrotask
來實現;同時還是需要考慮回撥內部執行報錯的場景,所以加上 try catch 來捕獲異常,並在異常的case,觸發 reject
基於上面的分析,我們就能敲出以下程式碼了
wrapCallback(promise2, callback, resolve, reject) {
return (arg) => queueMicrotask(() => {
let x;
try {
x = callback(arg);
} catch (error) {
reject(error);
return;
}
this.resolutionProcedure(promise2, x, resolve, reject)
})
}
OK,現在 wrapCallback
實現完畢!
終於,我們來到了最後一個方法 resolutionProcedure
,還記得這個流程是做什麼的嗎,先拋開其他細節,此流程主要是為了處理 x
是一個 thenable
的場景,以支援第三方實現的 類promise,滿足互操作性
的要求。
好吧,糾正一下我之前的措辭,在整個 Promise
實現中 resolutionProcedure
才是最難的(手動狗頭)
那麼接下來我們還是根據規範 條例2.3
來梳理出需要實現的邏輯點
- 因為這裡面存在自呼叫,所以當
p2
與x
是同一個物件時,為了防止死迴圈,需要直接退出後續處理並觸發reject
- 當
x
是一個 Promise 時,則直接將resolve & reject
包裝後註冊到x
上,由x
的最終狀態來決定p2
的狀態 - 當
x
是一個普通物件或者方法時,如果x
存在then
方法,則將其視為thenable
。注意,這裡需要考慮then
是一個 getter 的情況,也就是意味著在訪問x.then
時也可能會報錯,因此獲取 then 的過程也需要 try catch 包裹一下,報錯的情況直接觸發reject
。後續處理的目標和第2點一致,還是遵循resolve
和reject
只能觸發一次的原則,需考慮then
執行報錯的場景,這裡就不做贅述了 - 以上條件均不滿足時,則將
x
作為value
觸發resolve
接下來就是編碼時間~
resolutionProcedure(promise2, x, resolve, reject) {
if (promise2 === x) {
reject(new TypeError('promise2 === x'));
return;
}
if (x instanceof _Promise) {
x.then(
(value) => this.resolutionProcedure(promise2, value, resolve, reject),
(reason) => reject(reason)
);
return;
}
if (
x !== null &&
typeof x === 'object' ||
typeof x === 'function'
) {
let then;
try {
then = x.then;
} catch (error) {
reject(error);
return;
}
if (typeof then === 'function') {
let isCalledResolvePromise = false;
let isCalledRejectPromise = false;
const resolvePromise = (value) => {
if (isCalledResolvePromise || isCalledResolvePromise) {
return;
}
isCalledResolvePromise = true;
this.resolutionProcedure(promise2, value, resolve, reject)
}
const rejectPromise = (reason) => {
if (isCalledRejectPromise || isCalledResolvePromise) {
return;
}
isCalledRejectPromise = true;
reject(reason)
}
try {
then.call(
x,
resolvePromise,
rejectPromise
)
} catch (error) {
if (
!isCalledRejectPromise &&
!isCalledResolvePromise
) {
reject(error)
}
}
return;
}
}
resolve(x);
}
至此,我們的 _Promise
就全部編碼完畢,那它能否像原生的 Promise
正常工作呢,趕緊去測試一下吧!
當然也不用大家去手寫測試用例了,官網提供了一個 npm
包promises-aplus-tests
github地址 -> https://github.com/promises-aplus/promises-tests
根據官方的文件描述,要執行測試用例我們還需要匯出一個標準結構
那麼我們就按照要求匯出如下物件
module.exports = {
resolved(value) {
return new _Promise((resolve) => resolve(value))
},
rejected(reason) {
return new _Promise((_, reject) => reject(reason))
},
deferred() {
const ret = {};
ret.promise = new _Promise((resolve, reject) => {
ret.resolve = resolve;
ret.reject = reject
})
return ret;
}
}
值得一提的是,deferred
是不是與 Promise.withResolvers
的功能如出一轍
回到正題,安裝promises-aplus-tests
後,我們根據官網文件的測試指南配置一下測試指令
// package.json
...
"scripts": {
"test": "promises-aplus-tests ./core/Promise.cjs"
},
...
接下來就可以測試了,在控制檯輸入
pnpm run test
OK,872個用例全部透過
寫在最後
到這裡 Promise拆解計劃
終於完成,這個系列的階段性目標也達成了,希望這個系列能真正幫助到大家,從此不再受 Promise
的毒打~
那麼這期就到這裡,如果覺得有用的話記得點贊加關注
哦!
我們下期見!