歡迎來這裡 前端雜談, 聊聊前端
程式碼在github
《手寫 Promise》是一個經典的問題,基本上大家上手都可以按照自己的理解,寫出來一個 promise, 有一天個朋友問我,"手寫 Promise 要寫到什麼程度才是合格的 ?", 這也引起了我的興趣和思考, "怎麼樣的 Promise ,才是完美的呢 ? "
完美的 Promise
第一個問題就是怎麼樣才算是一個完美的 Promise 呢, 其實這個問題也不難,實現一個和原生 Promise "相同"的 Promsie,不就是完美的了, 那麼第二個問題也就來了,原生的 Promise 是按照什麼標準來實現的呢, 查閱了資料之後知道是按照 [Promises/A+] (https://promisesaplus.com/)標準來實現的, 具體的實現在 ECMA - sec-promise-objects 上有記載, 現在標準有了,我們就可以來實現一個"完美的 Promise"了
Promises/A+
接下來我們來看看Promises/A+
標準說了啥, 主要是兩部分,一個是名詞定義,一個是標準描述,其中標準描述由三個部分組成, 接下來我們簡單介紹下:
Terminology
這部分是名詞定義,主要是描述了各個名詞在標準中的定義
promise
: 是具有then
行為符合規範的方法的object
或function
, 這裡需要注意的是不是function
是then
,是function
中有then
方法thenable
: 是定義then
方法的object
或函式
,這個和上面promise
的區別在於then
是一個函式,不一定需要符合規範行為value
: 是任何合法的 javascript 值,包括undefined
、thenable
、promise
,這裡的value
包含了thenable
和promise
,結合下面的規範,會發現是一個可巢狀的關係exception
: 是一個通過throw
關鍵詞丟擲來的值reason
: 表示一個promise
狀態是rejected
的原因
Requirements
這部分是標準的定義,分為以下三個部分
Promise States
一個promise
必須是以下三種狀態之一
pending
- 可以轉變成
fulfilled
或者rejected
狀態
- 可以轉變成
fulfilled
- 需要存在一個
value
- 需要存在一個
rejected
- 需要存在一個
reason
- 需要存在一個
當狀態是fulfilled
或者 rejected
時,狀態不可以再變化成其他狀態,而value
和reason
也不可以再變化
The then
Method
這部分定義了 promise
中 then
方法的行為,then
方法是用來訪問promise
狀態變成fulfilled
或者 rejected
的value
或者reason
的, then
有兩個引數,如下:
promise.then(onFulfilled,onRejected)
onFulfilled
/onRejected
- 都是可選引數,如果這兩個引數不是函式型別,那麼忽略
- 在
promise
狀態變成fulfilled
/rejected
之後被呼叫,會帶上value
/reason
作為函式的引數 - 只會被呼叫一次
- 需要在
巨集任務
或者微任務
事件迴圈中完成。 注: 這裡對於執行時機的描述比較有趣,可以看看文件 2.2.4 - 兩個函式需要被繫結在
global this
上執行
- 同一個 Promise可以被多次
then
呼叫,then
中的onFulfilled
和onRejected
必須按照then
的呼叫順序呼叫 then
函式呼叫之後需要返回一個promise
, 這也是promise
可以鏈式呼叫then
的基礎promise2 = promise1.then(onFulfilled,onRejected)
- 如果
onFulfilled
或者onRejected
函式返回了值x
, 則執行 Promise Resolution Procedure - 如果
onFulfilled
或者onRejected
丟擲錯誤e
, 則promise2
的狀態是rejected
,並且reason
是e
- 如果
onFulfilled
或者onRejected
不是一個函式,而且promise1
的狀態已經確定fulfilled/rejected
, 則promise2
- 如果
The Promise Resolution Procedure
其實大體的標準部分在Promise States
和 The then Method
已經描述完了,這部分主要規定了一個抽象的操作promise resolution procedure
, 用來描述當then
的 onFulfilled
或者onRejected
返回值x
時,需要怎麼樣去進行操作,把表示式記為[[Resolve]](promise,x)
, 這部分也是整個 Promise 實現最複雜的部分,我們一起看看他規定了什麼
[[Resolve]](promise,x)
當
promise
和x
是同一個物件時,promise
為rejected
,reason
是TypeError
const promise = Promise.resolve().then(()=>promise); // TypeError
- 如果
x
是一個Promise時,則promise
的狀態要與x
同步 如果
x
是一個object
或者一個function
, 這部分是最複雜的- 首先要把
x.then
儲存在一箇中間變數then
, 為什麼要這麼做可以看文件 3.5,然後根據不同條件進行處理 - 如果獲取
x.then
的時候就丟擲錯誤e
,則promise
狀態變成rejected
,reason
是e
如果
then
是一個函式,那麼這就是我們定義裡面的thenable
, 這時候繫結x
為 this並呼叫then
,傳入promise
的resolvePromise
和rejectPromise
作為兩個引數then.call(x, resolvePromise, rejectPromise)
接下來判斷呼叫的結果
- 如果
resolvePromise
被呼叫,value
是y
, 則呼叫[[Resolve]](promise,y)
- 如果
rejectPromise
被呼叫,reason
是e
, 則promise
狀態變成rejected
,reason
是e
- 如果
resolvePromise
和rejectPromise
都被呼叫,則以第一個呼叫會準,後續的呼叫都被忽略 如果呼叫過程中丟擲了錯誤
e
- 如果丟擲之前
resolvePromise
或者rejectPromise
已經被呼叫了,那麼就忽略錯誤 - 後者的話,則
promise
狀態變成rejected
,reason
是e
- 如果丟擲之前
- 如果
- 如果
then
不是一個函式,那麼promise
狀態變成fulfilled
,value
是x
- 首先要把
- 如果
x
不是一個object
或者function
, 則promise
狀態變成fulfilled
,value
是x
這裡面最複雜的就是在 resolvePromise
被呼叫,value
是y
這部分,實現的是thenable
的遞迴函式
上面就是如何實現一個"完美"的 Promise 的規範了,總的來說比較複雜的是在The Promise Resolution Procedure
和對於錯誤和呼叫邊界的情況,下面我們將開始動手,實現一個"完美"的Promise
如何測試你的 Promise
前面介紹了 Promise/A+
規範, 那麼如何測試你的實現是完全實現了規範的呢, 這裡Promise/A+
提供了 [promises-tests
](https://github.com/promises-a...), 裡面目前包含872個測試用例,用於測試 Promise 是否標準
正文開始
首先說明下這邊是按照已完成的程式碼對實現 promise 進行介紹程式碼在這裡, 這裡使用的是最終版本,裡面註釋大致標明瞭實現的規則編號,其實整體來說經過了很多修改,如果要看整個便攜過程,可以commit history, 關注promise_2.js
和promise.js
兩個檔案
編寫的關鍵點
整體的實現思路主要就是上面的規範了,當然我們也不是說逐條進行實現,而是對規範進行分類,統一去實現:
- promise的狀態定義及轉變規則和基礎執行
- then的實現
- onFulfilled和onRejected的執行及執行時機
- thenable的處理
- promise和then及thenable中對於錯誤的處理
resolve
和reject
函式的呼叫次數問題
promise的狀態定義及轉變規則和基礎執行
const Promise_State = {
PENDING: "pending",
FULFILLED: "fulfilled",
REJECTED: "rejected",
};
class MyPromise {
constructor(executerFn) {
this.state = Promise_State.PENDING;
this.thenSet = [];
try {
executerFn(this._resolveFn.bind(this), this._rejectedFn.bind(this));
} catch (e) {
this._rejectedFn.call(this, e);
}
}
}
在建構函式中初始化狀態為pending
,並且執行傳入建構函式的executerFn
,傳入resovlePromise
、rejectePromise
兩個引數
然後我們接下去就要實現 resolvePromise
,rejectPromise
這兩個函式
_resolveFn(result) {
// 2.1.2
if (this._checkStateCanChange()) {
this.state = Promise_State.FULFILLED;
this.result = result;
this._tryRunThen();
}
}
_rejectedFn(rejectedReason) {
//2.1.3
if (this._checkStateCanChange()) {
this.state = Promise_State.REJECTED;
this.rejectedReason = rejectedReason;
this._tryRunThen();
}
}
_checkStateCanChange() {
//2.1.1
return this.state === Promise_State.PENDING;
}
這裡主要是通過_checkStateCanChange
判斷是否可執行的狀態,然後進行狀態變更,value
、reason
的賦值,然後嘗試執行then
方法註冊的函式
這時候我們的promise 已經可以這麼呼叫了
const p = new MyPromise((resolve,reject)=>{
resolve('do resolve');
// reject('do reject');
});
then的實現
接下來我們實現then
函式,首先有個簡單的問題: 『then方法是什麼時候執行的?』,有人會回答,是在 promise 狀態變成resolve
或者rejected
的之後執行的,這個乍一看好像沒毛病,但是其實是有毛病的,正確的說法應該是
『then方法是立即執行的,then方法傳入的onFulfilled
、onRejected
引數會在 promise 狀態變成resolve
或者rejected
後執行
我們先上程式碼
then(onFulfilled, onRejected) {
const nextThen = [];
const nextPromise = new MyPromise((resolve, reject) => {
nextThen[1] = resolve;
nextThen[2] = reject;
});
nextThen[0] = nextPromise;
//2.2.6
this.thenSet.push([onFulfilled, onRejected, nextThen]);
this._runMicroTask(() => this._tryRunThen());
return nextThen[0];
}
程式碼看起來也挺簡單的,主要邏輯就是構造一個新的 promise,然後把 onFulfilled
、onRejected
還有新構造的 promise 的resolve
、reject
儲存到thenSet
集合中,然後返回這個新構建的promise, 這時候我們的程式碼已經可以這樣子呼叫
const p = new MyPromise((resolve,reject)=>{
resolve('do resolve');
// reject('do reject');
});
p.then((value)=>{
console.log(`resolve p1 ${value}`);
},(reason)=>{
console.log(`reject p1 ${reason}`);
}).then((value)=>console.log(`resolve pp1 ${value}`));
p.then((value)=>{
console.log(`resolve p2 ${value}`);
},(reason)=>{
console.log(`reject p2 ${reason}`);
});
onFulfilled和onRejected的執行及執行時機
onFulFilled
和onRejected
會在 promise 狀態變成fulfilled
或者rejected
之後被呼叫,結合then
方法被呼叫的時機,判斷時候狀態可以呼叫需要在兩個地方做
- 在
resolvePromise
、resolvePromise
被呼叫的時候(判斷是否有呼叫了then註冊了onFulfilled
和onRejected
) 在
then
函式被呼叫的時候(判斷是否 promise狀態已經變成了fulfilled
或rejected
)這兩個時機會呼叫以下函式
_tryRunThen() { if (this.state !== Promise_State.PENDING) { //2.2.6 while (this.thenSet.length) { const thenFn = this.thenSet.shift(); if (this.state === Promise_State.FULFILLED) { this._runThenFulfilled(thenFn); } else if (this.state === Promise_State.REJECTED) { this._runThenRejected(thenFn); } } } }
這裡會判斷時候需要呼叫
then
註冊的函式,然後根據 promise 的狀態將thenSet
中的函式進行對應的呼叫
_runThenFulfilled(thenFn) {
const onFulfilledFn = thenFn[0];
const [resolve, reject] = this._runBothOneTimeFunction(
thenFn[2][1],
thenFn[2][2]
);
if (!onFulfilledFn || typeOf(onFulfilledFn) !== "Function") {
// 2.2.73
resolve(this.result);
} else {
this._runThenWrap(
onFulfilledFn,
this.result,
thenFn[2][0],
resolve,
reject
);
}
}
_runThenFulfilled
和_runThenRejected
相似,這裡就通過一個進行講解,
首先我們判斷onFulfilled
或者onRejected
的合法性
- 如果不合法則不執行,直接將promise 的
value
或reason
透傳給之前返回給then
的那個 promise,這個時候相當於then
的 promise 的狀態和原來的 promise 的狀態相同 - 如果合法,則執行
onFulfilled
或者onRejected
_runThenWrap(onFn, fnVal, prevPromise, resolve, reject) {
this._runMicroTask(() => {
try {
const thenResult = onFn(fnVal);
if (thenResult instanceof MyPromise) {
if (prevPromise === thenResult) {
//2.3.1
reject(new TypeError());
} else {
//2.3.2
thenResult.then(resolve, reject);
}
} else {
// ... thenable handler code
// 2.3.3.4
// 2.3.4
resolve(thenResult);
}
} catch (e) {
reject(e);
}
});
}
這裡先擷取一小段_runThenWrap
,主要是說明onFulfilled
或onRejected
的執行,這部分在規範中有這樣子的一個描述
onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
簡單來說就是onFulfilled
和onRejected
要在執行上下文裡面沒有除了platform code
之後才能執行,這段聽起來有點拗口,其實說人話就是我們經常說的要在微任務
、巨集任務
所以我們這裡包裝了_runMicroTask
方法,用於封裝這部分執行的邏輯
_runMicroTask(fn) {
// 2.2.4
queueMicrotask(fn);
}
這裡使用queueMicrotask
作為微任務的實現, 當然這個有相容性問題,具體可以看caniuse
實現的方法還有很多,比如setTimeout
、setImmediate
、 MutationObserver
、process.nextTick
然後將value
或reason
作為引數執行onFulfilled
或onRejected
,然後獲取返回值thenResult
,接下來就會有幾個判斷的分支
如果
thenResult
是一個 promise- 判斷是否和
then
返回的 promise 是相同的,如果是丟擲TypeError
- 傳遞
then
返回的 promise 的resolve
和reject
,作為thenResult.then
的onFulFilled
和onRejected
函式
- 判斷是否和
如果
thenResult
不是一個 promise- 判斷是否是
thenable
,這部分我們在下面進行講解 - 如果以上判斷都不是,那麼將
thenResult
作為引數,呼叫resolvePromise
- 判斷是否是
thenable的處理
thenable
應該說是實現裡面最複雜的一個部分了,首先,我們要根據定義判斷上部分的thenResult
是否是thenable
if (
typeOf(thenResult) === "Object" ||
typeOf(thenResult) === "Function"
) {
//2.3.3.1
const thenFunction = thenResult.then;
if (typeOf(thenFunction) === "Function") {
// is thenable
}
}
可以看到 需要判斷是否是一個Object
或者Function
,然後再判斷thenResult.then
是不是個 Function
,那麼有人會問,能不能寫成這樣子
if (
(typeOf(thenResult) === "Object" ||
typeOf(thenResult) === "Function") && (typeOf(thenResult.then) === 'Function')
) {
// is thenable
}
剛開始我也是這麼寫的,然後發現測試用例跑不過,最後去看了規範,有這麼一段3.5
簡單來說就是為了保證測試和呼叫的一致性,先把thenResult.then
進行儲存再判斷和執行是有必要的,多次訪問屬性可能會返回不同的值
接下去就是thenable
的處理邏輯了
簡單來說thenable
的處理邏輯有兩種情況
- 在 promise 的
then
或者resolve
中處理thenable
的情況 - 在
thenable
的then
回撥中處理value
還是thenable
的情況
這裡用在 promise 的then
的thenable
呼叫進行講述:
_thenableResolve(result, resolve, reject) {
try {
if (result instanceof MyPromise) {
// 2.3.2
result.then(resolve, reject);
return true;
}
if (typeOf(result) === "Object" || typeOf(result) === "Function") {
const thenFn = result.then;
if (typeOf(thenFn) === "Function") {
// 2.3.3.3
thenFn(resolve, reject);
return true;
}
}
} catch (e) {
//2.3.3.3.4
reject(e);
return true;
}
}
const [resolvePromise, rejectPromise] =
this._runBothOneTimeFunction(
(result) => {
if (!this._thenableResolve(result, resolve, reject)) {
resolve(result);
}
},
(errorReason) => {
reject(errorReason);
}
);
try {
thenFunction.call(thenResult, resolvePromise, rejectPromise);
} catch (e) {
//2.3.3.2
rejectPromise(e);
}
這裡我們構造了resolvePromise
和rejectPromise
,然後呼叫 thenFunction
, 在函式邏輯中處理完成之後將會呼叫resolvePromise
或者rejectPromise
, 這時候如果result
是一個 thenable
,那麼就會繼續傳遞下去,直到不是thenable
,呼叫resolve
或者reject
我們要注意的是 promise 的then
方法和thenable
的then
方法是有不同的地方的
- promise 的
then
有兩個引數,一個是fulfilled
,一個是rejected
,在前面的 promise狀態改變之後會回撥對應的函式 thenable
的then
也有兩個引數,這兩個引數是提供給thenable
呼叫完成進行回撥的resolve
和reject
方法,如果thenable
的回撥值還是一個thenable
,那麼會按照這個邏輯呼叫下去,直到是一個非thenable
,就會呼叫離thenable
往上回溯最近的一個 promies 的resolve
或者reject
到這裡,我們的promise 已經可以支援
thenable
的執行new MyPromise((resolve)=>{ resolve({ then:(onFulfilled,onRejected)=>{ console.log('do something'); onFulfilled('hello'); } }) }).then((result)=>{ return { then:(onFulfilled,onRejected)=>{ onRejected('world'); } } });
promise和then及thenable中對於錯誤的處理
錯誤處理指的是在執行過程中出現的錯誤要進行捕獲處理,基本上使用 try/catch
在捕獲到錯誤之後呼叫 reject
回撥,這部分比較簡單,可以直接看程式碼
resolve和reject函式的呼叫次數問題
一個 promise 中的resolve
和reject
呼叫可以說是互斥而且唯一的,就是這兩個函式只能有一個被呼叫,而且呼叫一次,這個說起來比較簡單,但是和錯誤場景在一起的時候,就會有一定的複雜性
本來可能是這樣子的
if(something true){
resolve();
}else {
reject();
}
加上錯誤場景之後
try{
if(something true){
resolve();
throw "some error";
}else {
reject();
}
}catch(e){
reject(e);
}
這時候判斷就會無效了, 因此我們按照通過一個工具類來包裝出兩個互斥的函式,來達到目的
_runBothOneTimeFunction(resolveFn, rejectFn) {
let isRun = false;
function getMutuallyExclusiveFn(fn) {
return function (val) {
if (!isRun) {
isRun = true;
fn(val);
}
};
}
return [
getMutuallyExclusiveFn(resolveFn),
getMutuallyExclusiveFn(rejectFn),
];
}
至此,我們一個完全符合Promise/A+
標準的 Promise,就完成了, 完整程式碼在這裡
等等,是不是少了些什麼
有人看到這裡會說,這就完了嗎?
我經常使用的catch
、finally
,還有靜態方法Promise.resolve
、Promise.reject
、Promise.all/race/any/allSettled
方法呢?
其實從標準來說,Promise/A+
的標準就是前面講述的部分,只定義了then
方法,而我們日常使用的其他方法,其實也都是在then
方法上面去派生的,比如catch
方法
MyPromise.prototype.catch = function (catchFn) {
return this.then(null, catchFn);
};
具體的方法其實也實現了,具體可以看promise_api
最後
最後是想分享下這次這個 promise 編寫的過程,從上面的講述看似很順利,但是其實在編寫的時候,我基本上是簡單了過了以下標準,然後按照自己的理解,結合promises-tests
單元測試用例來編寫的,這種開發模式其實就是TDD(測試驅動開發 (Test-driven development)),這種開發模式會大大減輕發人員程式設計時候對於邊界場景沒有覆蓋的心智負擔,但是反過來,對於測試用例的便攜質量要求就很高了
總體來說這次便攜 promise 是一個比較有趣的過程,上面如果有什麼問題的,歡迎留言多多交流