天天看點

手寫一個Promise/A+,完美通過官方872個測試用例

前段時間我用兩篇文章深入講解了異步的概念和Event Loop的底層原理,然後還講了一種自己實作異步的釋出訂閱模式:

setTimeout和setImmediate到底誰先執行,本文讓你徹底了解Event Loop

從釋出訂閱模式入手讀懂Node.js的EventEmitter源碼

本文會講解另一種更現代的異步實作方案:<code>Promise</code>。Promise幾乎是面試必考點,是以我們不能僅僅會用,還得知道他的底層原理,學習他原理的最好方法就是自己也實作一個Promise。是以本文會自己實作一個遵循<code>Promise/A+</code>規範的Promise。實作之後,我們還要用<code>Promise/A+</code>官方的測試工具來測試下我們的實作是否正确,這個工具總共有872個測試用例,全部通過才算是符合<code>Promise/A+</code>規範,下面是他們的連結:

<code>Promise/A+</code>規範: https://github.com/promises-aplus/promises-spec

<code>Promise/A+</code>測試工具: https://github.com/promises-aplus/promises-tests

本文的完整代碼托管在GitHub上: https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/JavaScript/Promise/MyPromise.js

Promise的基本用法,網上有很多,我這裡簡單提一下,我還是用三個互相依賴的網絡請求做例子,假如我們有三個網絡請求,請求2必須依賴請求1的結果,請求3必須依賴請求2的結果,如果用回調的話會有三層,會陷入“回調地獄”,用Promise就清晰多了:

上面的例子裡面,<code>then</code>是可以鍊式調用的,後面的<code>then</code>可以拿到前面<code>resolve</code>出來的資料,我們控制台可以看到三個success依次打出來:

通過上面的例子,其實我們已經知道了一個promise長什麼樣子,Promises/A+規範其實就是對這個長相進一步進行了規範。下面我會對這個規範進行一些講解。

<code>promise</code>:是一個擁有 <code>then</code> 方法的對象或函數,其行為符合本規範 <code>thenable</code>:是一個定義了 <code>then</code> 方法的對象或函數。這個主要是用來相容一些老的Promise實作,隻要一個Promise實作是thenable,也就是擁有<code>then</code>方法的,就可以跟Promises/A+相容。 <code>value</code>:指<code>reslove</code>出來的值,可以是任何合法的JS值(包括 <code>undefined</code> , thenable 和 promise等) <code>exception</code>:異常,在Promise裡面用<code>throw</code>抛出來的值 <code>reason</code>:拒絕原因,是<code>reject</code>裡面傳的參數,表示<code>reject</code>的原因

Promise總共有三個狀态:

<code>pending</code>: 一個promise在resolve或者reject前就處于這個狀态。 <code>fulfilled</code>: 一個promise被resolve後就處于<code>fulfilled</code>狀态,這個狀态不能再改變,而且必須擁有一個不可變的值(<code>value</code>)。 <code>rejected</code>: 一個promise被reject後就處于<code>rejected</code>狀态,這個狀态也不能再改變,而且必須擁有一個不可變的拒絕原因(<code>reason</code>)。

注意這裡的不可變指的是<code>===</code>,也就是說,如果<code>value</code>或者<code>reason</code>是對象,隻要保證引用不變就行,規範沒有強制要求裡面的屬性也不變。Promise狀态其實很簡單,畫張圖就是:

一個promise必須擁有一個<code>then</code>方法來通路他的值或者拒絕原因。<code>then</code>方法有兩個參數:

<code>onFulfilled</code> 和 <code>onRejected</code> 都是可選參數。

如果 <code>onFulfilled</code> 不是函數,其必須被忽略

如果 <code>onRejected</code> 不是函數,其必須被忽略

如果 <code>onFulfilled</code> 是函數:

當 <code>promise</code> 執行結束後其必須被調用,其第一個參數為 <code>promise</code> 的終值<code>value</code>

在 <code>promise</code> 執行結束前其不可被調用

其調用次數不可超過一次

如果 <code>onRejected</code> 是函數:

當 <code>promise</code> 被拒絕執行後其必須被調用,其第一個參數為 <code>promise</code> 的據因<code>reason</code>

在 <code>promise</code> 被拒絕執行前其不可被調用

<code>then</code> 方法可以被同一個 <code>promise</code> 調用多次

當 <code>promise</code> 成功執行時,所有 <code>onFulfilled</code> 需按照其注冊順序依次回調

當 <code>promise</code> 被拒絕執行時,所有的 <code>onRejected</code> 需按照其注冊順序依次回調

<code>then</code> 方法必須傳回一個 <code>promise</code> 對象。

如果 <code>onFulfilled</code> 或者 <code>onRejected</code> 傳回一個值 <code>x</code> ,則運作 Promise 解決過程:<code>[[Resolve]](promise2, x)</code>

如果 <code>onFulfilled</code> 或者 <code>onRejected</code> 抛出一個異常 <code>e</code> ,則 <code>promise2</code> 必須拒絕執行,并傳回拒因 <code>e</code>

如果 <code>onFulfilled</code> 不是函數且 <code>promise1</code> 成功執行, <code>promise2</code> 必須成功執行并傳回相同的值

如果 <code>onRejected</code> 不是函數且 <code>promise1</code> 拒絕執行, <code>promise2</code> 必須拒絕執行并傳回相同的據因

規範裡面還有很大一部分是講解Promise 解決過程的,光看規範,很空洞,前面這些規範已經可以指導我們開始寫一個自己的Promise了,Promise 解決過程會在我們後面寫到了再詳細講解。

我們自己要寫一個Promise,肯定需要知道有哪些工作需要做,我們先從Promise的使用來窺探下需要做啥:

建立Promise需要使用<code>new</code>關鍵字,那他肯定是作為面向對象的方式調用的,Promise是一個類。關于JS的面向對象更詳細的解釋可以看這篇文章。 我們<code>new Promise(fn)</code>的時候需要傳一個函數進去,說明Promise的參數是一個函數 構造函數傳進去的<code>fn</code>會收到<code>resolve</code>和<code>reject</code>兩個函數,用來表示Promise成功和失敗,說明構造函數裡面還需要<code>resolve</code>和<code>reject</code>這兩個函數,這兩個函數的作用是改變Promise的狀态。 根據規範,promise有<code>pending</code>,<code>fulfilled</code>,<code>rejected</code>三個狀态,初始狀态為<code>pending</code>,調用<code>resolve</code>會将其改為<code>fulfilled</code>,調用<code>reject</code>會改為<code>rejected</code>。 promise執行個體對象建好後可以調用<code>then</code>方法,而且是可以鍊式調用<code>then</code>方法,說明<code>then</code>是一個執行個體方法。鍊式調用的實作這篇有詳細解釋,我這裡不再贅述。簡單的說就是<code>then</code>方法也必須傳回一個帶<code>then</code>方法的對象,可以是this或者新的promise執行個體。

為了更好的相容性,本文就不用ES6了。

根據規範,<code>resolve</code>方法是将狀态改為fulfilled,<code>reject</code>是将狀态改為rejected。

最後将<code>resolve</code>和<code>reject</code>作為參數調用傳進來的參數,記得加上<code>try</code>,如果捕獲到錯誤就<code>reject</code>。

根據我們前面的分析,<code>then</code>方法可以鍊式調用,是以他是執行個體方法,而且規範中的API是<code>promise.then(onFulfilled, onRejected)</code>,我們先把架子搭出來:

那<code>then</code>方法裡面應該幹什麼呢,其實規範也告訴我們了,先檢查<code>onFulfilled</code>和<code>onRejected</code>是不是函數,如果不是函數就忽略他們,所謂“忽略”并不是什麼都不幹,對于<code>onFulfilled</code>來說“忽略”就是将<code>value</code>原封不動的傳回,對于<code>onRejected</code>來說就是傳回<code>reason</code>,<code>onRejected</code>因為是錯誤分支,我們傳回<code>reason</code>應該throw一個Error:

參數檢查完後就該幹點真正的事情了,想想我們使用Promise的時候,如果promise操作成功了就會調用<code>then</code>裡面的<code>onFulfilled</code>,如果他失敗了,就會調用<code>onRejected</code>。對應我們的代碼就應該檢查下promise的status,如果是<code>FULFILLED</code>,就調用<code>onFulfilled</code>,如果是<code>REJECTED</code>,就調用<code>onRejected</code>:

再想一下,我們建立一個promise的時候可能是直接這樣用的:

上面代碼<code>then</code>是在執行個體對象一建立好就調用了,這時候<code>fn</code>裡面的異步操作可能還沒結束呢,也就是說他的<code>status</code>還是<code>PENDING</code>,這怎麼辦呢,這時候我們肯定不能立即調<code>onFulfilled</code>或者<code>onRejected</code>的,因為<code>fn</code>到底成功還是失敗還不知道呢。那什麼時候知道<code>fn</code>成功還是失敗呢?答案是<code>fn</code>裡面主動調<code>resolve</code>或者<code>reject</code>的時候。是以如果這時候<code>status</code>狀态還是<code>PENDING</code>,我們應該将<code>onFulfilled</code>和<code>onRejected</code>兩個回調存起來,等到<code>fn</code>有了結論,<code>resolve</code>或者<code>reject</code>的時候再來調用對應的代碼。因為後面<code>then</code>還有鍊式調用,會有多個<code>onFulfilled</code>和<code>onRejected</code>,我這裡用兩個數組将他們存起來,等<code>resolve</code>或者<code>reject</code>的時候将數組裡面的全部方法拿出來執行一遍:

上面這種暫時将回調儲存下來,等條件滿足的時候再拿出來運作讓我想起了一種模式:訂閱釋出模式。我們往回調數組裡面<code>push</code>回調函數,其實就相當于往事件中心注冊事件了,<code>resolve</code>就相當于釋出了一個成功事件,所有注冊了的事件,即<code>onFulfilledCallbacks</code>裡面的所有方法都會拿出來執行,同理<code>reject</code>就相當于釋出了一個失敗事件。更多訂閱釋出模式的原理可以看這裡。

到這裡為止,其實我們已經可以實作異步調用了,隻是<code>then</code>的傳回值還沒實作,還不能實作鍊式調用,我們先來玩一下:

上述代碼輸出如下圖,符合我們的預期,說明到目前為止,我們的代碼都沒問題:

根據規範<code>then</code>的傳回值必須是一個promise,規範還定義了不同情況應該怎麼處理,我們先來處理幾種比較簡單的情況:

如果 <code>onFulfilled</code> 或者 <code>onRejected</code> 抛出一個異常 <code>e</code> ,則 <code>promise2</code> 必須拒絕執行,并傳回拒因 <code>e</code>。

如果 <code>onRejected</code> 不是函數且 <code>promise1</code> 拒絕執行, <code>promise2</code> 必須拒絕執行并傳回相同的據因。這個要求其實在我們檢測 <code>onRejected</code> 不是函數的時候已經做到了,因為我們預設給的<code>onRejected</code>裡面會throw一個Error,是以代碼肯定會走到catch裡面去。但是我們為了更直覺,代碼還是跟規範一一對應吧。需要注意的是,如果<code>promise1</code>的<code>onRejected</code>執行成功了,<code>promise2</code>應該被<code>resolve</code>。改造代碼如下:

如果 <code>onFulfilled</code> 或者 <code>onRejected</code> 傳回一個值 <code>x</code> ,則運作下面的 Promise 解決過程:<code>[[Resolve]](promise2, x)</code>。這條其實才是規範的第一條,因為他比較麻煩,是以我将它放到了最後。前面我們代碼的實作,其實隻要<code>onRejected</code>或者<code>onFulfilled</code>成功執行了,我們都要<code>resolve promise2</code>。多了這條,我們還需要對<code>onRejected</code>或者<code>onFulfilled</code>的傳回值進行判斷,如果有傳回值就要進行 Promise 解決過程。我們專門寫一個方法來進行Promise 解決過程。前面我們代碼的實作,其實隻要<code>onRejected</code>或者<code>onFulfilled</code>成功執行了,我們都要<code>resolve promise2</code>,這個過程我們也放到這個方法裡面去吧,是以代碼變為下面這樣,其他地方類似:

現在我們該來實作<code>resolvePromise</code>方法了,規範中這一部分較長,我就直接把規範作為注釋寫在代碼裡面了。

到這裡我們的Promise/A+基本都實作了,隻是還要注意一個點,如果使用者給構造函數傳的是一個同步函數,裡面的<code>resolve</code>和<code>reject</code>會立即執行,比<code>then</code>還執行的早,那<code>then</code>裡面注冊的回調就沒機會運作了,是以要給他們加個<code>setTimeout</code>:

我們使用Promise/A+官方的測試工具promises-aplus-tests來對我們的<code>MyPromise</code>進行測試,要使用這個工具我們必須實作一個靜态方法<code>deferred</code>,官方對這個方法的定義如下:

<code>deferred</code>: 傳回一個包含{ promise, resolve, reject }的對象 ​ <code>promise</code> 是一個處于<code>pending</code>狀态的promise ​ <code>resolve(value)</code> 用<code>value</code>解決上面那個<code>promise</code> ​ <code>reject(reason)</code> 用<code>reason</code>拒絕上面那個<code>promise</code>

我們實作代碼如下:

然後用npm将<code>promises-aplus-tests</code>下載下傳下來,再配置下package.json就可以跑測試了:

在跑測試的時候發現一個坑,在<code>resolvePromise</code>的時候,如果x是<code>null</code>,他的類型也是<code>object</code>,是應該直接用x來resolve的,之前的代碼會走到<code>catch</code>然後<code>reject</code>,是以需要檢測下<code>null</code>:

這個測試總共872用例,我們寫的Promise完美通過了所有用例:

在ES6的官方Promise還有很多API,比如:

Promise.resolve Promise.reject Promise.all Promise.race Promise.prototype.catch Promise.prototype.finally Promise.allSettled

雖然這些都不在Promise/A+裡面,但是我們也來實作一下吧,加深了解。其實我們前面實作了Promise/A+再來實作這些已經是小菜一碟了,因為這些API全部是前面的封裝而已。

将現有對象轉為Promise對象,如果 Promise.resolve 方法的參數,不是具有 then 方法的對象(又稱 thenable 對象),則傳回一個新的 Promise 對象,且它的狀态為fulfilled。

傳回一個新的Promise執行個體,該執行個體的狀态為rejected。Promise.reject方法的參數reason,會被傳遞給執行個體的回調函數。

該方法用于将多個 Promise 執行個體,包裝成一個新的 Promise 執行個體。

<code>Promise.all()</code>方法接受一個數組作為參數,<code>p1</code>、<code>p2</code>、<code>p3</code>都是 Promise 執行個體,如果不是,就會先調用<code>Promise.resolve</code>方法,将參數轉為 Promise 執行個體,再進一步處理。當p1, p2, p3全部resolve,大的promise才resolve,有任何一個reject,大的promise都reject。

用法:

該方法同樣是将多個 Promise 執行個體,包裝成一個新的 Promise 執行個體。上面代碼中,隻要<code>p1</code>、<code>p2</code>、<code>p3</code>之中有一個執行個體率先改變狀态,<code>p</code>的狀态就跟着改變。那個率先改變的 Promise 執行個體的傳回值,就傳遞給<code>p</code>的回調函數。

<code>Promise.prototype.catch</code>方法是<code>.then(null, rejection)</code>或<code>.then(undefined, rejection)</code>的别名,用于指定發生錯誤時的回調函數。

<code>finally</code>方法用于指定不管 Promise 對象最後狀态如何,都會執行的操作。該方法是 ES2018 引入标準的。

該方法接受一組 Promise 執行個體作為參數,包裝成一個新的 Promise 執行個體。隻有等到所有這些參數執行個體都傳回結果,不管是<code>fulfilled</code>還是<code>rejected</code>,包裝執行個體才會結束。該方法由 ES2020 引入。該方法傳回的新的 Promise 執行個體,一旦結束,狀态總是<code>fulfilled</code>,不會變成<code>rejected</code>。狀态變成<code>fulfilled</code>後,Promise 的監聽函數接收到的參數是一個數組,每個成員對應一個傳入<code>Promise.allSettled()</code>的 Promise 執行個體的執行結果。

完全版的代碼較長,這裡如果看不清楚的可以去我的GitHub上看:

https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/JavaScript/Promise/MyPromise.js

至此,我們的Promise就簡單實作了,隻是我們不是原生代碼,不能做成微任務,如果一定要做成微任務的話,隻能用其他微任務API模拟,比如<code>MutaionObserver</code>或者<code>process.nextTick</code>。下面再回顧下幾個要點:

Promise其實是一個釋出訂閱模式

<code>then</code>方法對于還在<code>pending</code>的任務,其實是将回調函數<code>onFilfilled</code>和<code>onRejected</code>塞入了兩個數組

Promise構造函數裡面的<code>resolve</code>方法會将數組<code>onFilfilledCallbacks</code>裡面的方法全部拿出來執行,這裡面是之前then方法塞進去的成功回調

同理,Promise構造函數裡面的<code>reject</code>方法會将數組<code>onRejectedCallbacks</code>裡面的方法全部拿出來執行,這裡面是之前then方法塞進去的失敗回調

<code>then</code>方法會傳回一個新的Promise以便執行鍊式調用

<code>catch</code>和<code>finally</code>這些執行個體方法都必須傳回一個新的Promise執行個體以便實作鍊式調用

文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝啬你的贊和GitHub小星星,你的支援是作者持續創作的動力。

歡迎關注我的公衆号進擊的大前端第一時間擷取高品質原創~

“前端進階知識”系列文章:https://juejin.im/post/5e3ffc85518825494e2772fd

“前端進階知識”系列文章源碼GitHub位址: https://github.com/dennis-jiang/Front-End-Knowledges

手寫一個Promise/A+,完美通過官方872個測試用例

繼續閱讀