原生es6封裝一個Promise對象


更新說明

  • 更新時間:2019/1/23

我把then方法的執行作成同步的了,是不符合規範的。javascript

[《Promises/A+規範》][6]中,【Then 方法】小節【調用時機】部分寫道:「onFulfilled 和 onRejected 只有在執行環境堆棧僅包含平臺代碼時纔可被調用」,這裏特別要看一下注釋。html

所以我要把onFulfilledonRejected 的代碼放在「 then 方法被調用的那一輪事件循環以後的新執行棧中執行」,經過setTimeout方法將任務放到本輪任務隊列的末尾。代碼已添加到最後一部分-第九步。java

關於任務隊列的運行機制,感興趣可看一下阮一峯老師的《JavaScript 運行機制詳解:再談Event Loop》git


實現功能:

  • 已實現 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方法是最難的,要不停地修修補補;
  • 最後全部功能都實現了,纔想起來一個關鍵點「Promise狀態一旦肯定,不能再改變」,又添加了一些邏輯才得以解決。所以,這個過程,難以作到滴水不漏,或許如今的代碼裏還有些隱藏問題沒被發現。
  • reject狀態的冒泡是個難題,但在下面的代碼中我沒有專門說起,我也沒有辦法具體說清楚他,我是在整個過程當中不停地調才最終調出來正確的冒泡結果。

代碼

下面貼代碼,包括整個思考過程,會有點長
爲了說明書寫的邏輯,我使用如下幾個註釋標識,整坨變更的代碼只標識這一坨的開頭處。
//++ ——添加的代碼
//-+ ——修改的代碼es6

第一步,定義MyPromise類

名字隨便取,個人叫MyPromise,沒有取代原生的Promise。github

  • 構造函數傳入回調函數 callback 。當新建 MyPromise 對象時,咱們須要運行此回調,而且 callback 自身也有兩個參數,分別是 resolvereject ,他們也是回調函數的形式;
  • 定義了幾個變量保存當前的一些結果與狀態、事件隊列,見註釋;
  • 執行函數 callback 時,若是是 resolve 狀態,將結果保存在 this.__succ_res 中,狀態標記爲成功;若是是 reject 狀態,操做相似;
  • 同時定義了最經常使用的 then 方法,是一個原型方法;
  • 執行 then 方法時,判斷對象的狀態是成功仍是失敗,分別執行對應的回調,把結果傳入回調處理;
  • 這裏接收 ...arg和傳入參數 ...this.__succ_res 都使用了擴展運算符,爲了應對多個參數的狀況,原封不動地傳給 then 方法回調。

callback 回調這裏使用箭頭函數,this 的指向就是本當前 MyPromise 對象,因此無需處理 this 問題。json

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 能夠簡單實現一些同步代碼,好比:segmentfault

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 中,這兩個例子已經能夠實現了。promise

  • 1 異步測試--resolve
  • 2 異步測試--reject

第三步,加入鏈式調用

實際上,原生的 Promise 對象的then方法,返回的也是一個 Promise 對象,一個新的 Promise 對象,這樣才能夠支持鏈式調用,一直then下去。。。 並且,then方法能夠接收到上一個then方法處理return的結果。根據Promise的特性分析,這個返回結果有3種可能:

  1. MyPromise對象;
  2. 具備then方法的對象;
  3. 其餘值。 根據這三種狀況分別處理。
  • 第一個處理的是,then方法返回一個MyPromise對象,它的回調函數接收resFnrejFn 兩個回調函數;
  • 把成功狀態的處理代碼封裝爲handle函數,接受成功的結果做爲參數;
  • handle函數中,根據onFulfilled返回值的不一樣,作不一樣的處理:
    • 首先,先獲取onFulfilled的返回值(若是有),保存爲returnVal
    • 而後,判斷returnVal是否有then方法,即包括上面討論的一、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...,因此我用它來包裹這些回調運行,捕獲到的錯誤進行相應處理。

爲確保代碼清晰,提取了resolverrejecter兩個函數,由於是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狀態屢次改變

第九步,onFulfilled 和 onRejected 方法異步執行

到這裏爲止,若是執行下面一段代碼,

function test30() {
  function fn30(resolve, reject) {
      console.log('running fn30');
      resolve('resolve @fn30')
  };
  console.log('start');
  let p = new MyPromise(fn30);
  p.then(res => {
      console.log(res);
  }).catch(err => {
      console.log('err=', err);
  });
  console.log('end');
};
複製代碼

輸出結果是:

//MyPromise結果
// start
// running fn30
// resolve @fn30
// end

//原生Promise結果:
// start
// running fn30
// end
// resolve @fn30
複製代碼

兩個結果不同,由於onFulfilled 和 onRejected 方法不是異步執行的,須要作如下處理,將它們的代碼放到本輪任務隊列的末尾執行。

function MyPromise(callback) {
    //略……

    var _this = this;
    function resolver(res) {
        setTimeout(() => {      //++ 利用setTimeout調整任務執行隊列
            if (_this.status === PENDING) {
                _this.status = FULFILLED;
                _this.__succ__res = res;
                _this.__queue.forEach(item => {
                    item.resolve(res);
                });
            };            
        }, 0);
    };
    function rejecter(rej) {
        setTimeout(() => {      //++
            if (_this.status === PENDING) {
                _this.status = REJECTED;
                _this.__err__res = rej;
                _this.__queue.forEach(item => {
                    item.reject(rej);
                });
            };            
        }, 0);
    };
    
    //略……
};
複製代碼

測試用例:

  • 30 then方法的異步執行

以上,是我全部的代碼書寫思路、過程。完整代碼與測試代碼到github下載


參考文章

相關文章
相關標籤/搜索