原生es6封裝一個Promise物件
- 已實現
Promise
基本功能,與原生一樣,非同步、同步操作均ok,具體包括:-
MyPromise.prototype.then()
-
MyPromise.prototype.catch()
與原生Promise
略有出入 -
MyPromise.prototype.finally()
-
MyPromise.all()
-
MyPromise.race()
-
MyPromise.resolve()
-
MyPromise.reject()
-
-
rejected
狀態的冒泡處理也已解決,當前Promise的reject如果沒有捕獲,會一直冒泡到最後,直到catch -
MyPromise
狀態一旦改變,將不能再改變它的狀態
不足之處:
- 程式碼的錯誤被catch捕獲時,提示的資訊(捕獲的錯誤物件)比原生Promise要多
- 程式碼是es6寫的,會考慮再用es5寫,以便於應用到es5專案中;es5寫的話,不用箭頭函式,要考慮this的問題
測試: index.html
- 這個頁面中包含了27個測試例子,分別測試了各項功能、各個方法,還有一些特殊情況測試;或許還有有遺漏的,感興趣自己可以玩一下;
- 視覺化的操作,方便測試,每次執行一個例子,開啟除錯臺即可看到結果;建議同時開啟
index.js
邊看程式碼邊玩; - 同一套程式碼,上面的
MyPromise
的執行結果,下面是原生Promise
執行的結果;
收穫
Promise then/catch reject
程式碼
下面貼程式碼,包括整個思考過程,會有點長
為了說明書寫的邏輯,我使用以下幾個註釋標識,整坨變動的程式碼只標識這一坨的開頭處。
//++
——新增的程式碼
//-+
——修改的程式碼
第一步,定義MyPromise類
名字隨便取,我的叫MyPromise,沒有取代原生的Promise。
- 建構函式傳入回撥函式
callback
。當新建MyPromise
物件時,我們需要執行此回撥,並且callback
自身也有兩個引數,分別是resolve
和reject
,他們也是回撥函式的形式; - 定義了幾個變數儲存當前的一些結果與狀態、事件佇列,見註釋;
- 執行函式
callback
時,如果是resolve
狀態,將結果儲存在this.__succ_res
中,狀態標記為成功;如果是reject
狀態,操作類似; - 同時定義了最常用的
then
方法,是一個原型方法; - 執行
then
方法時,判斷物件的狀態是成功還是失敗,分別執行對應的回撥,把結果傳入回撥處理; - 這裡接收
...arg
和傳入引數...this.__succ_res
都使用了擴充套件運算子,為了應對多個引數的情況,原封不動地傳給then
方法回撥。
callback
回撥這裡使用箭頭函式, this
的指向就是本當前 MyPromise
物件,所以無需處理 this
問題。
class MyPromise { constructor(callback) { this.__succ_res = null;//儲存成功的返回結果 this.__err_res = null;//儲存失敗的返回結果 this.status = 'pending';//標記處理的狀態 //箭頭函式綁定了this,如果使用es5寫法,需要定義一個替代的this callback((...arg) => { this.__succ_res = arg; this.status = 'success'; }, (...arg) => { this.__err_res = arg; this.status = 'error'; }); } then(onFulfilled, onRejected) { if (this.status === 'success') { onFulfilled(...this.__succ_res); } else if (this.status === 'error') { onRejected(...this.__err_res); }; } }; 複製程式碼
到這裡, MyPromise
可以簡單實現一些同步程式碼,比如:
new MyPromise((resolve, reject) => { resolve(1); }).then(res => { console.log(res); }); //結果 1 複製程式碼
第二步,加入非同步處理
執行非同步程式碼時, then
方法會先於非同步結果執行,上面的處理還無法獲取到結果。
- 首先,既然是非同步,
then
方法在pending
狀態時就執行了,所以新增一個else
; - 執行
else
時,我們還沒有結果,只能把需要執行的回撥,放到一個佇列裡,等需要時執行它,所以定義了一個新變數this.__queue
儲存事件佇列; - 當非同步程式碼執行完畢,這時候把
this.__queue
佇列裡的回撥統統執行一遍,如果是resolve
狀態,則執行對應的resolve
程式碼。
class MyPromise { constructor(fn) { this.__succ_res = null;//儲存成功的返回結果 this.__err_res = null;//儲存失敗的返回結果 this.status = 'pending';//標記處理的狀態 this.__queue = [];//事件佇列//++ //箭頭函式綁定了this,如果使用es5寫法,需要定義一個替代的this fn((...arg) => { this.__succ_res = arg; this.status = 'success'; this.__queue.forEach(json => {//++ json.resolve(...arg); }); }, (...arg) => { this.__err_res = arg; this.status = 'error'; this.__queue.forEach(json => {//++ json.reject(...arg); }); }); } then(onFulfilled, onRejected) { if (this.status === 'success') { onFulfilled(...this.__succ_res); } else if (this.status === 'error') { onRejected(...this.__err_res); } else {//++ this.__queue.push({resolve: onFulfilled, reject: onRejected}); }; } }; 複製程式碼
到這一步, MyPromise
已經可以實現一些簡單的非同步程式碼了。測試用例 index.html
中,這兩個例子已經可以實現了。
1 非同步測試--resolve 2 非同步測試--reject
第三步,加入鏈式呼叫
實際上,原生的 Promise
物件的then方法,返回的也是一個 Promise
物件,一個新的 Promise
物件,這樣才可以支援鏈式呼叫,一直 then
下去。。。 而且, then
方法可以接收到上一個 then
方法處理return的結果。根據 Promise
的特性分析,這個返回結果有3種可能:
MyPromise then
- 第一個處理的是,
then
方法返回一個MyPromise
物件,它的回撥函式接收resFn
和rejFn
兩個回撥函式; - 把成功狀態的處理程式碼封裝為
handle
函式,接受成功的結果作為引數; -
handle
函式中,根據onFulfilled
返回值的不同,做不同的處理:- 首先,先獲取
onFulfilled
的返回值(如果有),儲存為returnVal
; - 然後,判斷
returnVal
是否有then方法,即包括上面討論的1、2中情況(它是MyPromise
物件,或者具有then
方法的其他物件),對我們來說都是一樣的; - 之後,如果有
then
方法,馬上呼叫其then
方法,分別把成功、失敗的結果丟給新MyPromise
物件的回撥函式;沒有則結果傳給resFn
回撥函式。
- 首先,先獲取
class MyPromise { constructor(fn) { this.__succ_res = null;//儲存成功的返回結果 this.__err_res = null;//儲存失敗的返回結果 this.status = 'pending';//標記處理的狀態 this.__queue = [];//事件佇列 //箭頭函式綁定了this,如果使用es5寫法,需要定義一個替代的this fn((...arg) => { this.__succ_res = arg; this.status = 'success'; this.__queue.forEach(json => { json.resolve(...arg); }); }, (...arg) => { this.__err_res = arg; this.status = 'error'; this.__queue.forEach(json => { json.reject(...arg); }); }); } then(onFulfilled, onRejected) { return new MyPromise((resFn, rejFn) => {//++ if (this.status === 'success') { handle(...this.__succ_res);//-+ } else if (this.status === 'error') { onRejected(...this.__err_res); } else { this.__queue.push({resolve: handle, reject: onRejected});//-+ }; function handle(value) {//++ //then方法的onFulfilled有return時,使用return的值,沒有則使用儲存的值 let returnVal = onFulfilled instanceof Function && onFulfilled(value) || value; //如果onFulfilled返回的是新MyPromise物件或具有then方法物件,則呼叫它的then方法 if (returnVal && returnVal['then'] instanceof Function) { returnVal.then(res => { resFn(res); }, err => { rejFn(err); }); } else {//其他值 resFn(returnVal); }; }; }) } }; 複製程式碼
到這裡, MyPromise
物件已經支援鏈式呼叫了,測試例子: 4 鏈式呼叫--resolve
。但是,很明顯,我們還沒完成 reject
狀態的鏈式呼叫。
處理的思路是類似的,在定義的 errBack
函式中,檢查 onRejected
返回的結果是否含 then
方法,分開處理。值得一提的是,如果返回的是普通值,應該呼叫的是 resFn
,而不是 rejFn
,因為這個返回值屬於新 MyPromise
物件,它的狀態不因當前 MyPromise
物件的狀態而確定。即是,返回了普通值,未表明 reject
狀態,我們預設為 resolve
狀態。
程式碼過長,只展示改動部分。
then(onFulfilled, onRejected) { return new MyPromise((resFn, rejFn) => { if (this.status === 'success') { handle(...this.__succ_res); } else if (this.status === 'error') { errBack(...this.__err_res);//-+ } else { this.__queue.push({resolve: handle, reject: errBack});//-+ }; function handle(value) { //then方法的onFulfilled有return時,使用return的值,沒有則使用儲存的值 let returnVal = onFulfilled instanceof Function && onFulfilled(value) || value; //如果onFulfilled返回的是新MyPromise物件或具有then方法物件,則呼叫它的then方法 if (returnVal && returnVal['then'] instanceof Function) { returnVal.then(res => { resFn(res); }, err => { rejFn(err); }); } else {//其他值 resFn(returnVal); }; }; function errBack(reason) {//++ if (onRejected instanceof Function) { //如果有onRejected回撥,執行一遍 let returnVal = onRejected(reason); //執行onRejected回撥有返回,判斷是否thenable物件 if (typeof returnVal !== 'undefined' && returnVal['then'] instanceof Function) { returnVal.then(res => { resFn(res); }, err => { rejFn(err); }); } else { //無返回或者不是thenable的,直接丟給新物件resFn回撥 resFn(returnVal);//resFn,而不是rejFn }; } else {//傳給下一個reject回撥 rejFn(reason); }; }; }) } 複製程式碼
現在, MyPromise
物件已經很好地支援鏈式呼叫了,測試例子:
4 鏈式呼叫--resolve 5 鏈式呼叫--reject 28 then回撥返回Promise物件(reject) 29 then方法reject回撥返回Promise物件
第四步,MyPromise.resolve()和MyPromise.reject()方法實現
因為其它方法對 MyPromise.resolve()
方法有依賴,所以先實現這個方法。 先要完全弄懂 MyPromise.resolve()
方法的特性,研究了阮一峰老師的ECMAScript 6 入門對於 MyPromise.resolve()
方法的描述部分,得知,這個方法功能很簡單,就是把引數轉換成一個 MyPromise
物件,關鍵點在於引數的形式,分別有:
MyPromise thenable then
處理的思路是:
- 首先考慮極端情況,引數是undefined或者null的情況,直接處理原值傳遞;
- 其次,引數是
MyPromise
例項時,無需處理; - 然後,引數是其它
thenable
物件的話,呼叫其then
方法,把相應的值傳遞給新MyPromise
物件的回撥; - 最後,就是普通值的處理。
MyPromise.reject()
方法相對簡單很多。與 MyPromise.resolve()
方法不同, MyPromise.reject()
方法的引數,會原封不動地作為 reject
的理由,變成後續方法的引數。
MyPromise.resolve = (arg) => { if (typeof arg === 'undefined' || arg == null) {//無引數/null return new MyPromise((resolve) => { resolve(arg); }); } else if (arg instanceof MyPromise) { return arg; } else if (arg['then'] instanceof Function) { return new MyPromise((resolve, reject) => { arg.then((res) => { resolve(res); }, err => { reject(err); }); }); } else { return new MyPromise(resolve => { resolve(arg); }); } }; MyPromise.reject = (arg) => { return new MyPromise((resolve, reject) => { reject(arg); }); }; 複製程式碼
測試用例有8個: 18-25
,感興趣可以玩一下。
第五步,MyPromise.all()和MyPromise.race()方法實現
MyPromise.all()
方法接收一堆 MyPromise
物件,當他們都成功時,才執行回撥。依賴 MyPromise.resolve()
方法把不是 MyPromise
的引數轉為 MyPromise
物件。
每個物件執行 then
方法,把結果存到一個數組中,當他們都執行完畢後,即 i === arr.length
,才呼叫 resolve()
回撥,把結果傳進去。
MyPromise.race()
方法也類似,區別在於,這裡做的是一個 done
標識,如果其中之一改變了狀態,不再接受其他改變。
MyPromise.all = (arr) => { if (!Array.isArray(arr)) { throw new TypeError('引數應該是一個數組!'); }; return new MyPromise(function(resolve, reject) { let i = 0, result = []; next(); function next() { //如果不是MyPromise物件,需要轉換 MyPromise.resolve(arr[i]).then(res => { result.push(res); i++; if (i === arr.length) { resolve(result); } else { next(); }; }, reject); }; }) }; MyPromise.race = arr => { if (!Array.isArray(arr)) { throw new TypeError('引數應該是一個數組!'); }; return new MyPromise((resolve, reject) => { let done = false; arr.forEach(item => { //如果不是MyPromise物件,需要轉換 MyPromise.resolve(item).then(res => { if (!done) { resolve(res); done = true; }; }, err => { if (!done) { reject(err); done = true; }; }); }) }) } 複製程式碼
測試用例:
6 all方法 26 race方法測試
第六步,Promise.prototype.catch()和Promise.prototype.finally()方法實現
他們倆本質上是 then
方法的一種延伸,特殊情況的處理。
catch程式碼中註釋部分是我原來的解決思路:執行catch時,如果已經是錯誤狀態,則直接執行回撥;如果是其它狀態,則把回撥函式推入事件佇列,待最後接收到前面reject狀態時執行;因為catch直接收reject狀態,所以佇列中resolve是個空函式,防止報錯。
後來看了參考文章3才瞭解到還有更好的寫法,因此替換了。
class MyPromise { constructor(fn) { //...略 } then(onFulfilled, onRejected) { //...略 } catch(errHandler) { // if (this.status === 'error') { //errHandler(...this.__err_res); // } else { //this.__queue.push({resolve: () => {}, reject: errHandler}); ////處理最後一個Promise的時候,佇列resolve推入一個空函式,不造成影響,不會報錯----如果沒有,則會報錯 // }; return this.then(undefined, errHandler); } finally(finalHandler) { return this.then(finalHandler, finalHandler); } }; 複製程式碼
測試用例:
7 catch測試 16 finally測試——非同步程式碼錯誤 17 finally測試——同步程式碼錯誤
第七步,程式碼錯誤的捕獲
目前而言,我們的 catch
還不具備捕獲程式碼報錯的能力。思考,錯誤的程式碼來自於哪裡?肯定是使用者的程式碼,2個來源分別有:
-
MyPromise
物件建構函式回撥 -
then
方法的2個回撥 捕獲程式碼執行錯誤的方法是原生的try...catch...
,所以我用它來包裹這些回撥執行,捕獲到的錯誤進行相應處理。
為確保程式碼清晰,提取了 resolver
、 rejecter
兩個函式,因為是es5寫法,需要手動處理 this
指向問題
class MyPromise { constructor(fn) { this.__succ_res = null;//儲存成功的返回結果 this.__err_res = null;//儲存失敗的返回結果 this.status = 'pending';//標記處理的狀態 this.__queue = [];//事件佇列 //定義function需要手動處理this指向問題 let _this = this;//++ function resolver(...arg) {//++ _this.__succ_res = arg; _this.status = 'success'; _this.__queue.forEach(json => { json.resolve(...arg); }); }; function rejecter(...arg) {//++ _this.__err_res = arg; _this.status = 'error'; _this.__queue.forEach(json => { json.reject(...arg); }); }; try {//++ fn(resolver, rejecter);//-+ } catch(err) {//++ this.__err_res = [err]; this.status = 'error'; this.__queue.forEach(json => { json.reject(...err); }); }; } then(onFulfilled, onRejected) { //箭頭函式綁定了this,如果使用es5寫法,需要定義一個替代的this return new MyPromise((resFn, rejFn) => { function handle(value) { //then方法的onFulfilled有return時,使用return的值,沒有則使用回撥函式resolve的值 let returnVal = value;//-+ if (onFulfilled instanceof Function) {//-+ try {//++ returnVal = onFulfilled(value); } catch(err) {//++ //程式碼錯誤處理 rejFn(err); return; } }; if (returnVal && returnVal['then'] instanceof Function) { //如果onFulfilled返回的是新Promise物件,則呼叫它的then方法 returnVal.then(res => { resFn(res); }, err => { rejFn(err); }); } else { resFn(returnVal); }; }; function errBack(reason) { //如果有onRejected回撥,執行一遍 if (onRejected instanceof Function) { try {//++ let returnVal = onRejected(reason); //執行onRejected回撥有返回,判斷是否thenable物件 if (typeof returnVal !== 'undefined' && returnVal['then'] instanceof Function) { returnVal.then(res => { resFn(res); }, err => { rejFn(err); }); } else { //不是thenable的,直接丟給新物件resFn回撥 resFn(returnVal); }; } catch(err) {//++ //程式碼錯誤處理 rejFn(err); return; } } else {//傳給下一個reject回撥 rejFn(reason); }; }; if (this.status === 'success') { handle(...this.__succ_res); } else if (this.status === 'error') { errBack(...this.__err_res); } else { this.__queue.push({resolve: handle, reject: errBack}); }; }) } }; 複製程式碼
測試用例:
11 catch測試——程式碼錯誤捕獲 12 catch測試——程式碼錯誤捕獲(非同步) 13 catch測試——then回撥程式碼錯誤捕獲 14 catch測試——程式碼錯誤catch捕獲
其中第12個非同步程式碼錯誤測試,結果顯示是直接報錯,沒有捕獲錯誤,原生的 Promise
也是這樣的,我有點不能理解為啥不捕獲處理它。

第八步,處理MyPromise狀態確定不允許再次改變
這是 Promise
的一個關鍵特性,處理起來不難,在執行回撥時加入狀態判斷,如果已經是成功或者失敗狀態,則不執行回撥程式碼。
class MyPromise { constructor(fn) { this.__succ_res = null;//儲存成功的返回結果 this.__err_res = null;//儲存失敗的返回結果 this.status = 'pending';//標記處理的狀態 this.__queue = [];//事件佇列 //箭頭函式綁定了this,如果使用es5寫法,需要定義一個替代的this let _this = this; function resolver(...arg) { if (_this.status === 'pending') {//++ //如果狀態已經改變,不再執行本程式碼 _this.__succ_res = arg; _this.status = 'success'; _this.__queue.forEach(json => { json.resolve(...arg); }); }; }; function rejecter(...arg) { if (_this.status === 'pending') {//++ //如果狀態已經改變,不再執行本程式碼 _this.__err_res = arg; _this.status = 'error'; _this.__queue.forEach(json => { json.reject(...arg); }); }; }; try { fn(resolver, rejecter); } catch(err) { this.__err_res = [err]; this.status = 'error'; this.__queue.forEach(json => { json.reject(...err); }); }; } //...略 }; 複製程式碼
測試用例:
-
27 Promise狀態多次改變
以上,是我所有的程式碼書寫思路、過程。完整程式碼與測試程式碼到 ofollow,noindex">github 下載
參考文章
- ECMAScript 6 入門 - Promise 物件
- 3601" rel="nofollow,noindex">es6 promise原始碼實現
- 手把手教你實現一個完整的 Promise