Promise太重要了,可以說是改變了JavaScript開發體驗重要內容之一。而Promise也可以說是現代Javascript中極為重要的核心概念,所以理解Promise/A+規範,理解Promise的實現,手寫Promise就顯得格外重要。如果要聊Promise就要從回撥函數聊到回撥地獄,再聊到同步非同步,最終聊到Promise、async await。但是我們這篇文章,目的是手寫Promise,這些前置知識如果大家不瞭解的話,希望可以去補充一下。那你可能會說了,我他媽不懂你在說啥,我就是想手寫Promise,不行麼?大佬~~那肯定是沒問題的。好了,廢話不多說,咱們開始吧。
最開始的部分,我們不得不去看看Promise/A+規範,因為我們所實現的、手寫的程式碼,其實都是根據規範,一步一步來實現的。規範的地址,附在文末。
整個Promise/A+規範有三個部分:術語、要求、說明。術語部分介紹了核心欄位的關鍵性解釋,要求部分基本上就是對整個Promise實現的要求,說明部分,則對Promise的實現提供了一些特殊場景的補充說明。
首先,我們需要簡單瞭解下Promise的基本概念。Promise 是非同步程式設計的一種解決方案,比傳統的解決方案——回撥函數和事件——更合理和更強大。它由社群最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise
物件。與Promise互動的主要方式是通過它的then方法,該方法會註冊一個回撥函數,用來接收Promise的最終結果,這個結果可能是Promise的最終值,也可能是一個失敗的原因。也就是Promise的resolve和reject唄~
Promise/A+規範詳細說明了該then方法的行為,所有符合Promise/A+規範所實現的Promise都可以基於此來提供一個可以互相操作的基礎。因此,該規範應該是十分穩定的。也就是說,該Promise/A+規範並不會隨意迭代,提供了一個長期穩定不變的規範版本。當然,也可能會有小的修改,但是一定是經過深思熟慮的。額~這些都是廢話。重點就是最開始那一句:詳細說明了then方法的行為。記住這句話!記住這句話!記住這句話!因為我們後面所做的一切,除了基本結構和引數,所有的一切都是圍繞著then展開的。
最後,核心的Promise/A+規範並不會去管你怎麼實現,而是選擇專注於提供可互操作的then
方法。換句話說,我不管你寫怎麼實現Promise,我只關注你應該怎麼實現then方法的可操作性。
好了,基本的背景,我們瞭解了,我們之前說了,整個Promise/A+規範有三部分,我們先來看看第一部分術語,並實現這第一部分。
就這些,但是我稍微解釋下,promise是指Promise這個整體,它可以是一個建構函式,也可以是個物件。如果Promise是個建構函式的話,那麼thenable,其實就是Promise這個建構函式的範例,這個範例要有一個then方法。繼續,exception就是例外處理,通過try catch語句來丟擲錯誤。那麼最後,value和reason分別對應了resolve和reject的引數。
巴巴了這麼多,我們先來寫一點程式碼吧,不然長篇大論確實有點枯燥。
前面總結了一下Promise基本背景,還有基本的術語,我們這一小節就來實現基於此的程式碼,但是我先總結一下哈,我們要實現的到底包括哪些內容。
首先,我們要實現一個Promise類,這個類會提供一個實體方法也就是then方法。然後,我們還要有兩個值:value和reason。最後,我們還要處理丟擲的異常。沒啦~一句話概括,精煉!
1 class Promise { 2 constructor(excutor) { 3 this.value = undefined; 4 this.reason = undefined; 5 const resolve = (value) => {}; 6 const reject = (reason) => {}; 7 try { 8 excutor(resolve, reject); 9 } catch (err) { 10 reject(err); 11 } 12 } 13 then(onFulfilled, onRejected) {}
14 }
簡單不,其實啥也沒有,就是按照之前我們描述的規範,來一步一步實現的。我簡單再總結下,首先我們宣告了一個Promise類,然後這個類有一個實體方法then,這個then方法接收兩個回撥函數(後面我們會完善的),然後整個類別建構函式部分,宣告了一個value和reason,分別就是該Promise建構函式對應結果狀態的值。最後,我們通過try…catch語句執行傳遞進來的回撥excutor,並把resolve和reject方法作為excutor的回撥。這裡稍微有點繞,要捋一下噢。
那麼,我們再根據使用時的場景,來鞏固一下上面的程式碼,通常,我們在使用Promise的時候,是這樣的:
1 const p1 = new Promise((resolve, reject) => {});
我們看,使用的時候,我們會給Promise這個類傳過去一個方法,OK,這個方法就是我們Promise類中excutor。而這個executor又傳回去了兩個函數引數,可以讓我們在new Promise時傳遞的executor的內部去呼叫:
1 const p1 = new Promise((resolve, reject) => { 2 if(true){ 3 resolve("success") 4 } else { 5 reject("fail") 6 } 7 });
這樣我們的value和reason就傳遞給了constructor中的resolve和reject。最後,還有個實體方法then:
1 p1.then( 2 (value) => { 3 console.log("成功", value); 4 }, 5 (reason) => { 6 console.log("失敗", reason); 7 } 8 );
這個then方法有兩個引數,分別是成功的回撥函數和失敗的回撥函數,也就是分別代表了Promise類中的範例的then方法的onFulfilled, onRejected。那麼基本的結構我們就寫完了,但是現在我們寫的這個Promise肯定是沒法用的。所以,我們還得繼續往下,看看規範怎麼要求的,我們按照要求再來完善。
我們前面完成了基本的結構,但是那些程式碼還缺了一些內容,這一小節,我們就來根據後面的Promise/A+規範,也就是要求部分,來實現、完善後面的程式碼。首先,規範就對Promise的狀態,進行了詳細的描述。
promise必須處於以下三種狀態之一:pending, fulfilled, or rejected。也就是待處理、已完成或已拒絕。
1. 當處於pending狀態時:
2. 當處於fulfilled狀態時:
3. 當處於rejected狀態時:
OK,這就是關於Promise的狀態的描述。其實上面的說明很好理解,就是我們要給Promise這個類維護一個status欄位,這個status有三個狀態常數。一旦確定了status的結果,也就是status如果不是pending的話,就不能再修改status的狀態了。
我們繼續,針對then方法的詳細描述,當然,我們這個階段,只需要其中的一部分。首先,Promise必須提供一個then方法來讓使用者可以存取當前或最終狀態的value或reason。其次,Promise的then方法接受兩個引數:
promise.then(onFulfilled, onRejected)
誒嘿!就是我們上面寫的噢!繼續~注意,以下的列表標記,完全抄襲自Promise/A+規範,方便大家查詢。
2.2.1 onFulfilled和onRejected都是可選引數
2.2.1.1 如果onFulFilled不是函數,則忽略。
2.2.1.2 如果onRejected不是函數,則忽略。
2.2.2 如果onFulfilled是一個函數
2.2.2.1 onFulfilled必須在promise已經是fulfilled的狀態後呼叫,並且把promise的value作為它的第一個引數。
2.2.2.2 在promise已經是fulfilled狀態之前該方法不能被呼叫。
2.2.2.3 該方法只能被呼叫一次。
2.2.3 如果onRejected是一個函數
2.2.3.1 onRejected必須在promise已經是rejected的狀態後呼叫,並且把promise的reason作為它的第一個引數。
2.2.3.2 在promise已經是rejected狀態之前該方法不能被呼叫。
2.2.3.3 該方法只能被呼叫一次。
2.2.4 onFulfilled或者onRejected必須在執行上下文棧只有平臺程式碼的時候才可以被呼叫。
2.2.5 onFulfilled或者onRejected必須作為函數被呼叫(即沒有this值)。
OK,這一段規範翻譯,其中尤其要關注的是2.2.4和2.2.5。
首先,2.2.4是什麼意思呢?平臺程式碼,就是指環境、引擎、或者promise實現的程式碼,也就是說在onFulfilled或者onRejected被呼叫的時候,只能是在執行上下文棧中的最底層,它的前面要麼是全域性、要麼是另一個符合規範的promise。在實踐中,這樣可以確保在呼叫事件環之後非同步執行onFulfilled或者onRejected回撥。這可以通過宏任務(比如 setTimeout 或者setImmediate)或微任務(MutationObserver或者process.nextTick)機制來實現。由於promise的實現會被當作平臺程式碼,所以它本身可能包含一個任務排程佇列或者一個呼叫處理程式的「蹦床」。這裡的蹦床可以理解成一種轉換函數,比如遞迴可能會導致棧溢位,那麼可以通過蹦床函數把遞迴轉換成迴圈,繞過JS的棧溢位檢測機制。當然你也可能會寫成死迴圈,那就是你程式碼的問題了。
其次,2.2.5比較容易理解,就是指你實現的onFulfilled或者onRejected只能是一個函數,並且它內部的this如果在嚴格模式下是undefined,在非嚴格模式,則是全域性物件。換句話說,也就是你的onFulfilled或者onRejected不會被任何物件或者函數呼叫,不歸屬於任何物件或者函數。
理解了這段規範了吧?那麼我們開始手寫程式碼吧。
首先,根據規範,我們要加入三個狀態常數,並且Promise的初始狀態肯定是pending:
1 const PEDNING = "PENDING"; 2 const FULFILLED = "FULFILLED"; 3 const REJECTED = "REJECTED"; 4 class Promise { 5 constructor(excutor) { 6 this.status = PENDING; 7 this.value = undefined; 8 this.reason = undefined; 9 const resolve = (value) => {}; 10 const reject = (reason) => {}; 11 try { 12 excutor(resolve, reject); 13 } catch (err) { 14 reject(err); 15 } 16 } 17 then(onFulfilled, onRejected) {} 18 }
額外的,我們還要維護一個佇列,這個佇列用來當我們呼叫resolve或者reject的時候,觸發我們傳給then方法的函數引數。我們還要完善resolve和reject方法,以及then方法:
1 const PENDING = "PEDNING"; 2 const FULFILLED = "FULFILLED"; 3 const REJECTED = "REJECTED"; 4 class Promise { 5 constructor(excutor) { 6 this.status = PENDING; 7 this.value = undefined; 8 this.reason = undefined; 9 this.onResolvedCallbacks = []; 10 this.onRejectedCallbacks = []; 11 const resolve = (value) => { 12 if (this.status === PENDING) { 13 this.value = value; 14 this.status = FULFILLED; 15 this.onResolvedCallbacks.forEach((cb) => cb(this.value)); 16 } 17 }; 18 const reject = (reason) => { 19 if (this.status === PENDING) { 20 this.reason = reason; 21 this.status = REJECTED; 22 this.onRejectedCallbacks.forEach((cb) => cb(this.reason)); 23 } 24 }; 25 try { 26 excutor(resolve, reject); 27 } catch (err) { 28 reject(err); 29 } 30 } 31 then(onFulfilled, onRejected) { 32 if (this.status === FULFILLED) { 33 onFulfilled(this.value); 34 } 35 if (this.status === REJECTED) { 36 onRejected(this.reason); 37 } 38 if (this.status === PENDING) { 39 this.onResolvedCallbacks.push(onFulfilled); 40 this.onRejectedCallbacks.push(onRejected); 41 } 42 } 43 }
我們來看下上面的程式碼,第9、10行,我們分別維護了一個陣列,當然我們在promise仍舊是pending狀態的時候呼叫then方法,就會把這兩個狀態的回撥函數快取起來。等到我們呼叫resolve或者reject確認了promise的狀態時,就會去呼叫對應的快取佇列執行回撥。
resolve和reject方法十分簡單,繫結value或者reason,改變狀態,然後執行對應快取的回撥函數。而then方法的處理目前也並不複雜,根據狀態去呼叫對應回撥或者快取回撥。
注意,之前我們翻譯規範的時候還說過要確保在呼叫事件環之後非同步執行onFulfilled或者onRejected回撥。這個我們後面再實現,所以現在還都是同步程式碼。我們來測試一下我們的程式碼執行效果吧:
1 const p1 = new Promise((resolve, reject) => { 2 resolve("success"); 3 }); 4 5 p1.then( 6 (value) => { 7 console.log("成功", value); 8 }, 9 (reason) => { 10 console.log("失敗", reason); 11 } 12 ); 13 14 const p2 = new Promise((resolve, reject) => { 15 reject("fail"); 16 }); 17 18 p2.then( 19 (value) => { 20 console.log("成功", value); 21 }, 22 (reason) => { 23 console.log("失敗", reason); 24 } 25 );
我們來分析下這段程式碼是如何執行的。首先,當我們new Promise的時候就會執行Promise這個類的constructor,此時生成了部分範例欄位和resolve、reject方法。並且執行了excutor方法,那麼由於我們在方法內部呼叫resolve方法,也就相當於constructor是這樣執行的:
1 try { 2 function excutor(resolve,reject){ 3 resolve("success") 4 } 5 excutor(resolve, reject); 6 } catch (err) { 7 reject(err); 8 }
那麼此時,我們已經先走了resolve方法,也就是先確定了promised狀態,再強調一下,現在都是同步的噢,然後,當我們後面再去呼叫then方法的時候,promise的狀態已經是確定的了,所以不會去走快取,直接走了
if (this.status === FULFILLED) { onFulfilled(this.value); }
這段程式碼,於是就執行了傳給then的onFulfilled狀態的回撥。p2的例子也是一樣的。
那麼我們繼續再來看個例子:
1 const p1 = new Promise((resolve, reject) => { 2 setTimeout(() => { 3 resolve(1); 4 }, 1000); 5 }); 6 7 p1.then( 8 (value) => { 9 console.log(value); 10 }, 11 (reason) => { 12 console.log(reason); 13 } 14 );
這個例子的執行結果是什麼?我先不說答案,來捋一下,首先我們執行了excutor,但是此時我們並沒有呼叫resolve,因為是非同步的,所以要在一秒後才去呼叫resolve,那麼此時executor就執行完了,然後我們去執行then方法,此時狀態還沒確定,所以走then的時候就走了這段程式碼:
1 if (this.status === PENDING) { 2 this.onResolvedCallbacks.push(onFulfilled); 3 this.onRejectedCallbacks.push(onRejected); 4 }
快取了起來,那麼等到一秒後,呼叫了resolve,由於此時還是pending狀態,所以resolve就走了其內部的邏輯:
1 const resolve = (value) => { 2 if (this.status === PENDING) { 3 this.value = value; 4 this.status = FULFILLED; 5 this.onResolvedCallbacks.forEach((cb) => cb(this.value)); 6 } 7 };
所以一秒後才會列印出1。
那麼繼續,我們要進入下一個階段了,之前的階段我們解讀(就是把規範複製過來翻譯翻譯)的規範還欠了點債,就是非同步處理。那麼這一小節,我們繼續翻譯剩下的規範,並且把欠大家的債還上。
2.2.6 then方法可以被同一個promise呼叫多次
2.2.6.1 當promise是fulfilled狀態的時候,所有相關的onFulfilled回撥函數必須按照then方法呼叫的順序執行。
2.2.6.2 當promise是rejected狀態的時候,所有相關的onRejected回撥函數必須按照then方法呼叫的順序執行。
2.2.7 then方法必須返回一個promise
promise2 = promise1.then(onFulfilled, onRejected);
2.2.7.1 如果onFulfilled或者
onRejected的其中一個返回了一個值,x。那麼要執行:Promise Resolution Procedure
[[Resolve]](promise2, x)。
2.2.7.2 如果onFulfilled或者
onRejected其中一個丟擲了一個錯誤,e。promise2也必須變成使用e作為reason的拒絕狀態。
2.2.7.3 如果onFulfilled不是一個函數,並且promise1已經是fulfilled狀態,promise2也必須變成fulfilled狀態並且使用和promise1一樣的value作為值。
2.2.7.4 如果onRejected不是一個函數,並且promise1已經是rejected狀態,promise2也必須變成rejected狀態並且使用和promise1一樣的reason作為錯誤資訊。
OK,這就是我們這個階段要實現的規範。其中有些內容還是要解釋下的。首先就是讓人疑惑的Promise Resolution Procedure [[Resolve]](promise2, x),
這個東西我特意沒有翻譯,因為你需要把它看作一個整體,現在這個階段,你可以把它理解成一段要處理特定邏輯的程式碼塊。
那麼然後就是2.2.7.2到2.2.7.4,實際上只說了一句話,then方法必須返回一個promise,並且一旦該promise的狀態已經確定,後續的promise也一定是同樣的狀態不得更改,且要把源promise的value或reason依次向後傳遞。也就是所謂的值穿透,聽起來高大上,實際上沒啥複雜的概念。
我們接下來寫程式碼吧。哦對,2.2.6其實我們上一小節已經寫完了,就是那個陣列,每次呼叫then如果promise是pending狀態,就會往兩個陣列中依照then呼叫的順序依次往裡新增回撥。所以,我們這一小節,實際上需要處理的核心內容,就是then方法,或者,我可以在這裡確切的告訴大家,Promise的核心就是這個then方法,Promise中核心的核心是resolvePromise方法,接下來你就知道我為什麼這麼說了。
constructor的程式碼暫時沒有變化,這裡就不再複製了,我們僅特別重要的關注then的變化,那麼第一件要做的就是then方法會返回一個promise:
1 then(onFulfilled, onRejected) { 2 let p = new Promise((resolve, reject) => { 3 if (this.status === FULFILLED) { 4 onFulfilled(this.value); 5 } 6 if (this.status === REJECTED) { 7 onRejected(this.reason); 8 } 9 if (this.status === PENDING) { 10 this.onResolvedCallbacks.push(onFulfilled); 11 this.onRejectedCallbacks.push(onRejected); 12 } 13 }) 14 return p; 15 }
啥也沒幹哈,就是返回了個新的promise,其實這也就是promise的鏈式呼叫。那問題來了,為啥我要把then的內容用一個新的promise包裹起來呢?既然要返回個promise,那我直接在程式碼的最後面,比如這樣:
1 then(onFulfilled, onRejected) { 2 if (this.status === FULFILLED) { 3 onFulfilled(this.value); 4 } 5 if (this.status === REJECTED) { 6 onRejected(this.reason); 7 } 8 if (this.status === PENDING) { 9 this.onResolvedCallbacks.push(onFulfilled); 10 this.onRejectedCallbacks.push(onRejected); 11 } 12 let p = new Promise((resolve, reject) => { 13 }) 14 return p; 15 }
那我這樣不就可以了麼?別忘了,我們還要把promise1的結果,傳遞給promise2,所以我們通過再生成一個新的promise2的內部去執行我們的回撥。
那麼我們要如何把promise1的結果傳給下一層呢?我們回頭看下2.2.7.1,結果叫做x:
1 then(onFulfilled, onRejected) { 2 let p = new Promise((resolve, reject) => { 3 if (this.status === FULFILLED) { 4 setTimeout(() => { 5 try { 6 let x = onFulfilled(this.value); 7 resolvePromise(p, x, resolve, reject); 8 } catch (error) { 9 reject(error); 10 } 11 }, 0); 12 } 13 if (this.status === REJECTED) { 14 setTimeout(() => { 15 try { 16 let x = onRejected(this.reason); 17 resolvePromise(p, x, resolve, reject); 18 } catch (error) { 19 reject(error); 20 } 21 }, 0); 22 } 23 if (this.status === PENDING) { 24 this.onResolvedCallbacks.push(() => { 25 setTimeout(() => { 26 try { 27 let x = onFulfilled(this.value); 28 resolvePromise(p, x, resolve, reject); 29 } catch (error) { 30 reject(error); 31 } 32 }, 0); 33 }); 34 this.onRejectedCallbacks.push(() => { 35 setTimeout(() => { 36 try { 37 let x = onRejected(this.reason); 38 resolvePromise(p, x, resolve, reject); 39 } catch (error) { 40 reject(error); 41 } 42 }, 0); 43 }); 44 } 45 }); 46 return p; 47 }
OK,這就是這一小節完整的then方法的部分了,甚至是整個promise實現的then方法也基本上跟這個差不多了,我們先來看當呼叫then方法的時候已經是fulfilled狀態了的話,我們會獲取到我們傳給promise1的onFulfilled回撥的結果作為x,這就是我們之前規範裡所說的x,然後它走了一個resolvePromise,傳了四個引數,並且把我們外面的那個p傳了進去,那麼假設,我沒寫外面的setTimeout,我能把p傳給resolvePromise方法麼?不能!!!因為我們想要在p還未生成之前就傳給了內部的resolvePromise方法,這時候還沒有p呢,所以我們利用事件迴圈機制,包了一層setTimeout,這樣等到下一個tik整個p生成,我們就可以傳給resolvePromise了。
那麼我們注意,無論在哪一個狀態中,兩種結果狀態也就是fulfilled或rejected時,只要拿到了結果,就會走resolvePromise方法,只有try……catch報錯了,才會直接丟擲異常。那麼這裡的resolvePromise,其實就是我們前面沒有翻譯的那一段邏輯:Promise Resolution Procedure [[Resolve]](promise2, x)
,就這個。換句話說promise1的任何確定的狀態結果,對於promise2來說都是要去resolve的,除非執行promise2的時候報錯了。
簡單總結下:
那麼這一小節,我們要實現完整的Promise,其實就是完善resolvePromise方法啦。在寫程式碼之前,我們還是要來解讀一下規範,我們就是照著規範寫程式碼啦。
Promise Resolution Procedure是一個會接收promise和x作為引數的一個抽象操作,我們把它標記為:[[Resolve]](promise, x)。如果x的表現形式是thenable的,也就是說如果x的結果是一個promise的話,那麼假設x的行為與promise相似,則會試圖使promise採用x的狀態。
這種thenable的處理允許promise之間的互相操作,只要這些promise的實現符合本規範的要求。它同樣允許符合本規範的實現使用合理的then方法「同化」不符合的實現。
[[Resolve]](promise, x)程式需要遵循以下步驟:
2.3.1 如果promise和x參照自同一個物件,那麼則丟擲一個TypeError作為reason的reject狀態。
2.3.2 如果x是一個promise,則採用它的狀態。通常,只有當x的實現是符合本規範的要求時,才會知道它是不是一個真正的承諾。本條規則允許使用特定於實現的方法來採用已知符合promise的狀態。
2.3.2.1 如果x是pending的狀態,那麼promise也必須保持pending狀態,直到x的狀態已變更為fulfilled或者rejected。
2.3.2.2 如果x是fulfilled狀態,那麼就把promise的狀態變更為fulfilled並使用與x一樣的value。
2.3.2.3 如果x是rejected狀態,那麼就把promise的狀態變更為rejected並使用與x一樣的reason。
2.3.3 如果x是一個物件或者函數
2.3.3.1 宣告一個then變數儲存x.then方法。這樣做,其實就是為了快取一下x.then的參照,那麼當我們後續測試該參照,呼叫該參照的時候,都避免了多次呼叫x.then的存取。這些預防措施對於確保存取器屬性的一致性非常重要,因為存取器屬性的值可能在兩次檢索之間發生變化。
2.3.3.2 如果我們在執行x.then後丟擲了一個異常e,那麼promise狀態應修改為以e作為引數的rejected狀態。
2.3.3.3 如果then是一個函數,那麼則呼叫then.call,把x作為call的第一個引數(也就是作為then的this),resolvePromise作為第二個引數,rejectPromise作為第三個引數,其中:
2.3.3.3.1 如果resolvePromise被呼叫,並且使用y作為value,就執行[[Resolve]](promise, y)。
2.3.3.3.2 如果rejectPromise被呼叫,並且使用r作為reason,就讓promise的狀態變為rejected。
2.3.3.3.3 如果同時呼叫resolvePromise和
rejectPromise
或多次呼叫同一個,則第一個呼叫優先,並且任何進一步的呼叫都將被忽略。
2.3.3.3.4 如果呼叫then方法丟擲了一個錯誤e。
2.3.3.3.4.1 如果resolvePromise
或rejectPromise已經被呼叫了,那麼則忽略它。
2.3.3.3.4.2 否則,把promise的狀態變更為rejected,e作為reason。
2.3.3.4 如果then不是一個一個函數,那麼則把promise的狀態變更為fulfilled,把x作為value。
2.3.4 如果x不是一個函數或者物件,那麼則把promise的狀態變更為fulfilled,把x作為value。
如果一個promise被一個參與迴圈的thenable鏈中的thenable所resolved,這樣[[Resolve]](promise,thenable)的遞迴性質將最終導致再次呼叫[[Resolve]](promise,thenable),遵循上訴演演算法將導致無限遞迴。本規範鼓勵但是並不要求一定要去檢測這種無限遞迴,如果實現的話,那麼此時promise的狀態應該變成rejected並提供有效的錯誤資訊作為reason。
從實現上來說,不應該對可呼叫鏈的深度做任何限制,並假設超出該限制,遞迴將是無限的。只有確定的迴圈呼叫才會丟擲TypeError。如果遇到無限的不同thenable鏈,則無限遞迴是正確的。
好吧,這規範巴巴了好多。我建議要看一遍!我們繼續上一小節的內容,去完善resolvePromise方法。
首先2.3.1說了如果promise和x參照自同一個物件,那麼丟擲錯誤,這個簡單,就這樣被:
1 function resolvePromise(promise, x, resolve, reject) { 2 if (promise == x) { 3 return reject( 4 new TypeError("Chaining cycle detected for promise #<Promise>") 5 ); 6 } 7 }
那這樣的場景什麼時候才會出現呢?為什麼x和promise不能相等呢?我們先來看個例子,首先我們在判斷條件中新增一個列印,確定走到這個邏輯了:
1 if (promise == x) { 2 console.log("進來了"); 3 return reject( 4 new TypeError("Chaining cycle detected for promise #<Promise>") 5 ); 6 }
然後例子是這樣的:
1 const p1 = new Promise((resolve, reject) => { 2 resolve("success"); 3 }); 4 5 let p2 = p1.then((res) => { 6 return p2; 7 }); 8 p2.then( 9 (value) => { 10 console.log("成功", value); 11 }, 12 (reason) => { 13 console.log("失敗", reason); 14 } 15 );
執行這段程式碼,列印結果如下:
進來了 失敗 TypeError: Chaining cycle detected for promise #<Promise>
說明我們的程式碼沒問題,那麼我們得來分析下這段程式碼是如何執行的。在宣告p1的時候,我們直接執行了excutor的resolve,所以此時的promise就已經是fulfilled的狀態了。當我們再去呼叫then方法的時候,then方法會返回個promise,此時由於已經是fulfilled狀態了,所以會命中if (this.status === FULFILLED)條件,那麼此時的x就是呼叫onFulfilled方法的結果,這個x最終的結果就是p2,所以此時resolvePromise(p, x, resolve, reject)方法中的p和x是同一個,那麼在Promise內部,我們要等待p2的執行結果,那麼此時p2就即是執行的過程,又是等待的結果,所以p2永遠都不會處於結果狀態,於是我們要特殊處理這樣無法得到最終結果的情況,reject出去。理解了吧?
我們繼續,再往後是2.3.2的解釋,這塊的解釋我們在寫程式碼的時候可以不去考慮,因為在寫2.3.3的時候,可以覆蓋到2.3.2的邏輯。那麼:
1 function resolvePromise(promise, x, resolve, reject) { 2 if (promise == x) { 3 console.log("進來了"); 4 return reject( 5 new TypeError("Chaining cycle detected for promise #<Promise>") 6 ); 7 } 8 if ((typeof x === "object" && x !== null) || typeof x === "function") { 9 } else { 10 resolve(x); 11 } 12 }
著三行程式碼,就是2.3.3和2.3.4的邏輯框架,如果x是一個物件且不是null或者x是一個函數,那麼要走一段邏輯,如果不符合這個條件的話說明是一個普通值,直接resolve就好了。這塊很好理解,跟規範描述的一模一樣。
繼續,2.3.3.1和2.3.3.2,當我們去取x.then的時候,可能會有報錯,所以我們要try……catch處理一下:
1 if ((typeof x === "object" && x !== null) || typeof x === "function") { 2 try { 3 let then = x.then 4 } catch (error) { 5 reject(error) 6 } 7 } else { 8 resolve(x); 9 }
就是這樣。繼續2.3.3.3:
1 if ((typeof x === "object" && x !== null) || typeof x === "function") { 2 try { 3 let then = x.then; 4 if (typeof then === "function") { 5 then.call( 6 x, 7 (y) => { 8 resolve(y) 9 }, 10 (r) => { 11 reject(r); 12 } 13 ); 14 } else { 15 resolve(x); 16 } 17 } catch (error) { 18 reject(error); 19 } 20 } else { 21 resolve(x); 22 }
我們看這段程式碼,跟2.3.3.3所說是一模一樣的。我們呼叫then方法,把x作為this並且傳了onFulfilled, onRejected兩個函數作為引數,那麼基本的核心邏輯我們就處理完了,但是還要補全一些細節,比如2.3.3.3.3所說,如果多次呼叫,只取第一次的,後面的呼叫都應該被忽略,那這個邏輯我們要怎麼處理呢?再比如2.3.3.3.1所說,then在call的時候,如果resolvePromise被呼叫,並且使用y作為value,就執行[[Resolve]](promise, y),而不是像我們上面那樣,直接resolve就完事了。所以我們要把resolve(y)修改成resolvePromise(promise, y, resolve, reject)。因為我們可能再返回一個promise,直到最後不是一個promise,也就是規範中所說的,允許任意遞迴呼叫。
1 if ((typeof x === "object" && x !== null) || typeof x === "function") { 2 let called = false; 3 try { 4 let then = x.then; 5 if (typeof then === "function") { 6 then.call( 7 x, 8 (y) => { 9 if (called) return; 10 called = true; 11 resolvePromise(promise, y, resolve, reject); 12 }, 13 (r) => { 14 if (called) return; 15 called = true; 16 reject(r); 17 } 18 ); 19 } else { 20 resolve(x); 21 } 22 } catch (error) { 23 if (called) return; 24 called = true; 25 reject(error); 26 } 27 } else { 28 resolve(x); 29 }
我們看程式碼,其實處理起來也並不複雜,我們在執行的時候,新增了一個flag,只要走到了某一個會修改promise狀態的邏輯中,就會修改flag的狀態,後續再呼叫就不會再去走邏輯程式碼了。也就是一旦狀態確定,後續無論怎麼呼叫,都是之前確定的狀態,無法更改。
那麼其實到現在,核心的程式碼基本上就都完事了,其實你看,核心的程式碼其實就是resolvePromise這個方法,剩下的,我們還需要處理一點細節,比如2.2.1,onFulfilled, onRejected都是可選引數。我們稍微來處理一下then方法:
1 then(onFulfilled, onRejected) { 2 onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) => v; 3 onRejected = 4 typeof onRejected === "function" 5 ? onRejected 6 : (e) => { 7 throw e; 8 }; 9 // 後面的略了 10 }
看到沒,程式碼很簡單,如果沒傳,我們就自己造一個就好了。最後我們就寫完了這個Promise的實現,完整程式碼可在:https://github.com/zakingwong/zaking-js-advanced/tree/promise這裡檢視。
那麼最後,我們還可以使用社群的工具庫,來測試下我們所寫的程式碼是否符合規範。這個我就不多說了,具體可以參考gitHub上的程式碼。
既然程式碼我們都寫完了,來玩兩個例子吧。
1 const zp1 = new ZakingPromise((resolve, reject) => { 2 resolve("ok"); 3 }).then((data) => { 4 return new Promise((resolve, reject) => { 5 setTimeout(() => { 6 resolve(data); 7 }, 1000); 8 }); 9 }); 10 11 zp1.then( 12 (data) => { 13 console.log(data, "zp1-resolve"); 14 }, 15 (err) => { 16 console.log(data, "zp1-reject"); 17 } 18 );
這裡的ZakingPromise是我們自己手寫的Promise,Promise就是ES6的Promise。我們來分析下這段程式碼,其實並不難噢。首先,ZakingPromise直接resolve出去了一個ok字串,那麼當我們呼叫then的時候,狀態已經是fulfilled了,此時then裡面執行的程式碼是這樣的:
1 let p = new Promise((resolve, reject) => { 2 if (this.status === FULFILLED) { 3 setTimeout(() => { 4 try { 5 let x = onFulfilled(this.value); 6 resolvePromise(p, x, resolve, reject); 7 } catch (error) { 8 reject(error); 9 } 10 }, 0); 11 } 12 } 13 return p
pending和rejected都跟我沒關係。就執行了上面這點程式碼,那麼這裡的onFulfilled就是我們傳進來的這段程式碼:
1 (data) => { 2 return new Promise((resolve, reject) => { 3 setTimeout(() => { 4 resolve(data); 5 }, 1000); 6 }); 7 }
它返回了一個new Promise,所以此時的x可以理解為:
let x = new Promise((resolve, reject) => { setTimeout(() => { resolve('ok'); }, 1000); });
OK,那麼我們繼續,要去走resolvePromise這段程式碼了。那麼resolvePromise判斷邏輯就不走了哈,它肯定是個函數然後獲取x.then,並且x.then肯定也是函數。那麼裡面的邏輯就相當於是這樣執行的:
1 new Promise((resolve, reject) => { 2 setTimeout(() => { 3 resolve("ok"); 4 }, 1000); 5 }).then( 6 (y) => { 7 if (called) return; 8 called = true; 9 resolvePromise(promise, y, resolve, reject); 10 }, 11 (r) => { 12 if (called) return; 13 called = true; 14 reject(r); 15 } 16 );
這樣是不是就很好理解了,那麼我們就得去分析下上面的程式碼,還沒呼叫then是如何執行的,就很簡單了對吧,在Promise內部由於呼叫then的時候還沒resolve,之前咱們分析過,所以then方法記憶體了兩個快取陣列,當1秒後呼叫了resolve("ok")的時候,then裡的onFulfilled回撥就執行了,此時的y就是「ok」。注意,這裡我省略了那個呼叫call時候的x,也就是此時then方法內部的this只想,那這個x、也就是this就是指這個new Promise他自己。
於是我們再走這段程式碼:
1 zp1.then( 2 (data) => { 3 console.log(data, "zp1-resolve"); 4 }, 5 (err) => { 6 console.log(data, "zp1-reject"); 7 } 8 );
就走了zp1的onFulfilled回撥,於是就列印了「ok zp1-resolve」。行吧,例子就這一個了吧暫時,大家要去多練練例子啊,把複雜的情況都梳理梳理。
誒?我記得Promise還有catch方法、all方法、還有race、還有finally,這些方法你咋沒寫呢?嗯,首先前四章所有翻譯的內容就是Promise規範的所有內容,不信的話我在文末貼出了Promise/A+規範的地址,也就是說規範中壓根就沒有這些方法,這些方法都是ES6額外實現的,那麼接下來我們就來實現下這些方法。
我們先來看下Promise.resolve和Promise.reject,其實實現起來很簡單哈,我們先寫個例子:
1 Promise.resolve("ok").then((data) => { 2 console.log(data, "resolve"); 3 });
我們看,呼叫Promise.resolve直接就返回了個promise的fulfilled狀態,再去呼叫then的成功的回撥,所以實現起來就是這樣的:
1 static resolve(value) { 2 return new Promise((resolve, reject) => { 3 resolve(value); 4 }); 5 }
前面的那些程式碼我就不寫了哈,但是這樣還沒完,我們再看個例子:
1 Promise.resolve( 2 new Promise((resolve, reject) => { 3 setTimeout(() => { 4 resolve("Zaking"); 5 }, 1000); 6 }) 7 ).then((data) => { 8 console.log(data, "resolve----"); 9 });
你猜,按照我們之前實現的程式碼,這個列印的結果是什麼。
Promise { status: 'PENDING', value: undefined, reason: undefined, onResolvedCallbacks: [], onRejectedCallbacks: [] } resolve----
為什麼會這樣呢?按道理不應該是Zaking這個字串麼?ES6實現的是這樣的,但是咱們之前的程式碼並沒有做這個的處理,其實這裡的resolve方法,不就是包裹了一層Promise後執行了resolve嘛,所以當我們呼叫Promise.resolve的時候,上面例子的程式碼中,傳給Promise.resolve的那個新的promise被當作value返回了,所以這裡我們要新增點程式碼處理下:
1 class Promise { 2 constructor(excutor) { 3 // ... 4 const resolve = (value) => { 5 if (value instanceof Promise) { 6 return value.then(resolve, reject); 7 } 8 if (this.status === PENDING) { 9 this.value = value; 10 this.status = FULFILLED; 11 this.onResolvedCallbacks.forEach((cb) => cb(this.value)); 12 } 13 }; 14 // ... 15 } 16 // ... 17 }
我們判斷一下傳入的value是不是一個Promise的範例,如果是的話,那麼就再呼叫一下then,這樣就可以把結果傳遞到下一層了。這樣,我們列印的結果就符合ES6的實現了,注意,這個跟規範無關了噢。
Zaking resolve----
你猜Promise.reject這個方法怎麼實現?我不寫了噢,你自己試試!要注意的是,resolve一個Promise會等待解析後的結果,但是reject一個Promise會直接走向失敗。
那麼接下來我們來實現一個更簡單的方法,Promise.catct:
1 catch(errCb) { 2 return this.then(null, errCb); 3 }
就這麼簡單,其實catch方法本質上就是then中的onRejected這個回撥,那麼我們直接呼叫就好了,不傳onFulfilled只傳onRejected。
all方法其實很好理解,就是所有的回撥都成功了,才算是成功,我們看個例子:
1 Promise.all([ 2 new Promise((resolve, reject) => { 3 resolve("1"); 4 }), 5 new Promise((resolve, reject) => { 6 resolve("2"); 7 }), 8 ]).then((data) => { 9 console.log(data, "all"); 10 });
就是這樣,all方法會接收一個都是Promise的陣列,然後內部會去處理這個陣列,我們就來看看是怎麼處理的吧:
1 static all(promises) { 2 let result = []; 3 let times = 0; 4 return new Promise((resolve, reject) => { 5 function processResult(data, index) { 6 result[index] = data; 7 if (++times === promises.length) { 8 resolve(result); 9 } 10 } 11 for (let i = 0; i < promises.length; i++) { 12 const promise = promises[i]; 13 Promise.resolve(promise).then((data) => { 14 processResult(data, i); 15 }, reject); 16 } 17 }); 18 }
其實你看程式碼並不多的,我們來分析下吧。首先我們宣告了兩個變數,一個儲存結果的陣列,這個結果是指resolve成功後的onFulfilled回撥的value,一個是計數器。然後我們去迴圈整個傳入的promises,讓每一個promise傳遞給Promise.resolve去執行,然後我們用一個processResult去處理返回的data和當前的下標i,然後我們還要處理一下reject,那麼這裡要注意哈,對於all方法來說,只要有一個出錯了,那麼整個all執行的結果就是rejected的,所以reject不用特殊處理,直接reject就好了,不需要新增進陣列這個那個的。
那麼繼續,processResult每當我們執行一次的時候,就會根據傳入的下標的位置去儲存結果,這樣處理其實就是為了按照傳入promise的先後順序去儲存結果,然後先++times再去和promises的length長度去做比較,換句話說就是計數嘛,如果相等,就直接resolve最終的result即可。並不是很複雜噢。
這個race是什麼意思呢,我們先看個例子:
const p = Promise.race([p1, p2, p3]);
上面程式碼中,只要p1
、p2
、p3
之中有一個範例率先改變狀態,p
的狀態就跟著改變。那個率先改變的 Promise 範例的返回值,就傳遞給p
的回撥函數。換句話說就是,只要有一個結果就行了,不管這個結果是啥。誰跑得快我就算誰是第一,這就是race的意思。那既然是誰快我選誰,那我們程式碼要怎麼寫?迴圈一下全部執行一遍唄,誰有結果了就完事了唄:
1 static race(promises) { 2 return new Promise((resolve, reject) => { 3 for (let i = 0; i < promises.length; i++) { 4 let promise = promises[i]; 5 Promise.resolve(promise).then(resolve, reject); 6 } 7 }); 8 }
嗯……就這麼簡單,其實就是all的那部分刪除了一些~~
這個方法是啥意思呢,就是不管成功或失敗,會返回所有非同步的結果。誒?那不是跟all方法很類似,只要修改一下all方法不就可以了,沒錯,你可真是個小聰明:
1 static allSettled(promises) { 2 let result = []; 3 let times = 0; 4 return new Promise((resolve, reject) => { 5 function processResult(data, index, status) { 6 result[index] = { status, value: data }; 7 if (++times === promises.length) { 8 resolve(result); 9 } 10 } 11 for (let i = 0; i < promises.length; i++) { 12 const promise = promises[i]; 13 Promise.resolve(promise).then( 14 (data) => { 15 processResult(data, i, "fulfilled"); 16 }, 17 (err) => { 18 processResult(err, i, "rejected"); 19 } 20 ); 21 } 22 }); 23 }
你看跟all方法有啥區別?無非就是額外處理了onRejected,直接onRejected就直接失敗了,現在回去走processResult方法也存到result中,當然,result存的內容也不太一樣,多了個狀態,會告訴你是失敗的結果還是成功的結果。不復雜吧,其實就是all方法。
finally方法用於指定不管Promise物件最後的狀態如何,都會執行的操作,就是我不管你Promise最後是fulfilled還是rejected,都會執行finally的回撥。那我們來看是咋實現的吧:
1 finally(finallyCallback) { 2 let p = this.constructor; 3 return this.then( 4 (data) => { 5 return p.resolve(finallyCallback()).then(() => data); 6 }, 7 (err) => { 8 return p.resolve(finallyCallback()).then(() => { 9 throw err; 10 }); 11 } 12 ); 13 }
其實程式碼不難,但是這裡面還是有點東西的。為什麼我在this.then中又呼叫了p.resolve並且傳了finally的finallyCallback回撥?我直接這樣寫不行麼?
1 finally(finallyCallback) { 2 return this.then( 3 (data) => { 4 finallyCallback() 5 return data; 6 }, 7 (err) => { 8 finallyCallback() 9 throw err; 10 } 11 ); 12 }
呼叫finallyCallback,返回結果。好像挺完美的。但是,不太行,因為按照上面的finally實現,你這樣去寫Promise的用法:
1 Promise.resolve("zaking-finally") 2 .finally(() => { 3 console.log("finally~~~"); 4 }) 5 .then((data) => { 6 console.log(data, "finally-resolved"); 7 }) 8 .catch((err) => { 9 console.log(err, "finally-rejected"); 10 });
或者這樣:
1 Promise.resolve("zaking-finally") 2 .finally(() => { 3 console.log("finally~~~"); 4 return 1; 5 }) 6 .then((data) => { 7 console.log(data, "finally-resolved"); 8 }) 9 .catch((err) => { 10 console.log(err, "finally-rejected"); 11 });
都沒問題,但是因為finallyCallback有可能是非同步的,所以我們需要額外的包裹一層,再去執行最終的onFulfilled或者onRejected。比如這樣:
1 Promise.resolve("zaking-finally") 2 .finally(() => { 3 return new Promise((resolve, reject) => { 4 setTimeout(() => { 5 resolve(); 6 console.log("finally"); 7 }, 1000); 8 }); 9 }) 10 .then((data) => { 11 console.log(data, "finally-resolved"); 12 }) 13 .catch((err) => { 14 console.log(err, "finally-rejected"); 15 });
那麼本章所有的內容就都完事了,當然我們並沒有實現所有的方法,如果你真的理解了,完全可以自己去實現了,比如any方法和try方法,甚至於你可以自己去創造某些方法,因為Promise/A+規範範圍內的所有內容,其實就那麼點,其他的都是實現罷了,你看我講了五個方法,實際上只講了all和finally這兩個方法。
最後我們來聊一聊promisify,也是這篇文章最後的一點內容,promisify其實是Node.js提供的可以把普通帶回撥的函數,轉換成promise物件的工具方法。我們先來看個例子,怎麼用promisify:
1 function testA(a, b, cb) { 2 cb(null, a + b); 3 } 4 5 let A = promisify(testA); 6 A(1, 2).then( 7 (data) => { 8 console.log(data, "data-----"); 9 }, 10 (err) => { 11 console.log(err); 12 } 13 );
上面的程式碼,首先testA是一個帶有回撥函數作為引數的方法,當然這是一個同步回撥,然後要注意的是,cb在testA中必須是最後一個引數,而執行cb的時候,第一個引數必須是錯誤處理的回撥函數,這裡我們直接用null代替了,然後,我們通過promisify方法,包裹了一下testA,生成了一個A方法,那麼此時的A就是一個Promise物件了噢,然後呼叫Promise的then方法,傳入onFulfilled和onRejected兩個回撥,其實這裡的onFulfilled就可以理解成是cb,而onRejected就是那個null。那麼我們來看下實現,程式碼很少:
1 function promisify(fn) { 2 return function (...args) { 3 return new Promise((resolve, reject) => { 4 fn(...args, function (err, data) { 5 if (err) return reject(err); 6 resolve(data); 7 }); 8 }); 9 }; 10 }
好嘞,那麼我們依照實現和例子程式碼,我們來分析下這個promisify是如何執行的。首先,promisify返回了一個函數,那麼這個返回的函數,就是我們例子中的A方法,A傳入的引數1,2也同樣在promisify返回的function中通過rest引數來處理的,從而獲取到我們傳入的引數。然後,我們在返回的函數內部又返回了一個Promise。我們先不管這個Promise,咱們刪除點東西再看一下:
1 function promisify(fn) { 2 return function (...args) { 3 return new Promise((resolve, reject) => { 4 fn(); 5 }); 6 }; 7 }
我們看,其實就是在返回的Promise內部執行了一下我們傳入的testA方法,只不過他回撥前面的引數都用rest引數的方式獲取到了,那麼我們testA的第三個引數就是這裡的:
1 function (err, data) { 2 if (err) return reject(err); 3 resolve(data); 4 }
這一部分,就是testA內部執行的cb了唄。err就是我們傳的null,data就是那個a + b。簡單吧?其實一點都不復雜。
那你可能會問為啥要這樣固定的去寫testA方法呢,回撥必須是最後一個引數,錯誤的回撥還必須是回撥函數的第一個引數,嗯。。是因為node的實現是這樣的~~
最後,再補充一點,這個東西理解了promisify後就很簡單了,不多說了,也就是promisifyAll方法,接收一個物件作為引數:
1 function promisifyAll(obj) { 2 let result = {}; 3 for (let key in obj) { 4 result[key] = 5 typeof obj[key] === "function" ? promisify(obj[key]) : obj[key]; 6 } 7 return result; 8 }
很簡單,就是生成一個新的物件result,如果傳入的obj中的某個key是函數,就走一下promisify方法轉換,最終返回result結果物件。
附:
最後:由於本人能力有限,感覺寫的內容並未完全覆蓋使用場景,大家見諒,但是還是有參考價值的~額~~然後我第一次嘗試加行號,結果部落格園的行號會一起復制下來,不太友好,所以特別建議大家手打程式碼,嘿嘿。