解析 Promise 原理,實現一個Promise

概述

這篇文章旨在解析 Promise的異步實現原理,並且以 ES6中的 Promise 爲藍本實現一個簡單的 Promise。

通過自己動手實現一個 Promise 對象,可以熟悉很多可能不知道的 Promise 細節,同時也能對異步的理解更提升一步。

本文假設讀者對 Promise 規範有一定理解,並且熟悉 ES6 中的 Promise 基本操作。

Promise 核心

  • Promise 概括來說是對異步的執行結果的描述對象。(這句話的理解很重要)

  • Promise 規範中規定了,promise 的狀態只有3種:

    1. pending
    2. fulfilled
    3. rejected

顧名思義,對上面3個狀態的解釋就不再贅述,Promise 的狀態一旦改變則不會再改變

  • Promise 規範中還規定了 Promise 中必須有 then 方法,這個方法也是實現異步的鏈式操作的基本。

具體的規範可以參見:https://promisesaplus.com

ES6 Promise細節

  1. Promise 構造器中必須傳入函數,否則會拋出錯誤。(沒有執行器還怎麼做異步操作。。。)
  2. Promise.prototype上的 catch(onrejected) 方法是 then(null,onrejected) 的別名,並且會處理鏈之前的任何的reject。
  3. Promise.prototype 上的 then和 catch 方法總會返回一個全新的 Promise 對象
  4. 如果傳入構造器的函數中拋出了錯誤,該 promise 對象的[[PromiseStatus]]會賦值爲 rejected,並且[[PromiseValue]]賦值爲 Error 對象。
  5. then 中的回調如果拋出錯誤,返回的 promise 對象的[[PromiseStatus]]會賦值爲 rejected,並且[[PromiseValue]]賦值爲 Error 對象。
  6. then 中的回調返回值會影響 then 返回的 promise 對象。(下文會具體分析)

這部分內容參考: http://es6.ruanyifeng.com/#docs/promise

動手實現

做了上面的鋪墊,實現一個 Promise 的思路就清晰很多了,本文使用 ES6 來進行實現,暫且把這個類取名爲 GPromise吧(不覆蓋原生的,便於和原生進行對比測試)。下文中 GPromise 代指將要實現的類,Promise 代指 ES6中的 Promise 類。

內部屬性

在瀏覽器中打印出一個 Promise 實例會發現其中會包括兩用”[[ ]]”包裹起來的屬性,這是系統內部屬性,只有JS 引擎能夠訪問。

[[PromiseStatus]]
[[PromiseValue]]

以上兩個屬性分別是 Promise 對象的狀態和最終值。

我們自己不能實現內部屬性,JS中私有屬性特性(#修飾符現在還是提案)暫時也沒有支持,所以暫且用”_”前綴規定私有屬性,這樣就模擬了Promise 中的兩個內部屬性。

class GPromise {
        constructor(executor) {
            this._promiseStatus = GPromise.PENDING;
            this._promiseValue;
            this.execute(executor);
        }

        execute(executor){
            //...
        }

        then(onfulfilled, onrejected){
            //...
        }
    }

    GPromise.PENDING = 'pedding';
    GPromise.FULFILLED = 'resolved';
    GPromise.REJECTED = 'rejected';

執行器

  1. 傳入構造器的executor爲函數,並且在構造時就會執行。
  2. 我們給 executor 中傳入 resolve 和 reject 參數,這兩個參數都是函數,用於改變改變 _promiseStatus和 _promiseValue 的值。
  3. 並且內部做了捕獲異常的操作,一旦傳入的executor 函數執行拋出錯誤,GPromise 實例會變成 rejected狀態,即 _promiseStatus賦值爲’rejected’,並且 _promiseValue賦值爲Error對象。
  execute(executor) {
            if (typeof executor != 'function') {
                throw new Error(` GPromise resolver ${executor} is not a function`);
            }
            //捕獲錯誤
            try {
                executor(data => {
                    this.promiseStatus = GPromise.FULFILLED;
                    this.promiseValue = data;
                }, data => {
                    this.promiseStatus = GPromise.REJECTED;
                    this.promiseValue = data; 
                });
            } catch (e) {
                this.promiseStatus = GPromise.REJECTED;
                this.promiseValue = e;
            }
        }

注:Promise 對象在executor 發生錯誤或者reject 時,如果沒有then
或者 catch 來處理,會把錯誤拋出到外部,也就是會報錯。GPromise 實現的是沒有向外部拋出錯誤,只能由then方法處理。

then方法

異步實現

then 方法內部邏輯稍微複雜點,並且有一點一定一定一定要注意到: then 方法中的回調是異步執行的,思考下下段代碼:

console.log(1);
new Promise((resolve,reject)=>{
    console.log(2);
    resolve();
})
.then(()=>console.log(3));
console.log(4);

執行結果是什麼呢?答案其實是:1 2 4 3。傳入Promise 中的執行函數是立即執行完的啊,爲什麼不是立即執行 then 中的回調呢?因爲then 中的回調是異步執行,表示該回調是插入事件隊列末尾,在當前的同步任務結束之後,下次事件循環開始時執行隊列中的任務。

then 方法中的難點就是處理異步,其中一個方案是通過 setInterval來監聽GPromise 對象的狀態改變,一旦改變則執行相應then 中相應的回調函數(onfulfilled和onrejected),這樣回調函數就能夠插入事件隊列末尾,異步執行,實驗證明可行,這種方案是最直觀也最容易理解的。

then 返回值

then 方法的返回值是一個新的 GPromise 對象,並且這個對象的狀態和 then 中的回調返回值相關,回調指代傳入的 onfulfilled 和 rejected。
1. 如果 then 中的回調拋出了錯誤,返回的 GPromise 的 _promiseStatus 賦值爲’rejected’, _promiseValue賦值爲拋出的錯誤對象。
2. 如果回調返回了一個非 GPromise 對象, then返回的 GPromise 的 _promiseStatus 賦值爲’resolved’, _promiseValue賦值爲回調的返回值。
3. 如果回調返回了一個 GPromise 對象,then返回的GPromise對象 的_promiseStatus和 _promiseValue 和其保持同步。也就是 then 返回的GPromise記錄了回調返回的狀態和值,不是直接返回回調的返回值。

代碼

then 方法中的重點邏輯如上,其他參見代碼即可:

  then(onfulfilled, onrejected) {
            let _ref = null,
                timer = null,
                result = new GPromise(() => {});

            //因爲 promiseexecutor 是異步操作,需要監聽 promise 對象狀態變化,並且不能阻塞線程
            timer = setInterval(() => {
                if ((typeof onfulfilled == 'function' && this._promiseStatus == GPromise.FULFILLED) ||
                    (typeof onrejected == 'function' && this._promiseStatus == GPromise.REJECTED)) {
                    //狀態發生變化,取消監聽
                    clearInterval(timer);
                    //捕獲傳入 then 中的回調的錯誤,交給 then 返回的 promise 處理
                    try {
                        if (this._promiseStatus == GPromise.FULFILLED) {
                            _ref = onfulfilled(this._promiseValue);
                        } else {
                            _ref = onrejected(this._promiseValue);
                        }

                        //根據回調的返回值來決定 then 返回的 GPromise 實例的狀態
                        if (_ref instanceof GPromise) {
                            //如果回調函數中返回的是 GPromise 實例,那麼需要監聽其狀態變化,返回新實例的狀態是根據其變化相應的
                            timer = setInterval(()=>{
                                if (_ref._promiseStatus == GPromise.FULFILLED ||
                                    _ref._promiseStatus == GPromise.REJECTED) {
                                    clearInterval(timer);
                                    result._promiseValue = _ref._promiseValue;
                                    result._promiseStatus = _ref._promiseStatus;
                                }
                            },0);

                        } else {
                            //如果返回的是非 GPromise 實例
                            result._promiseValue = _ref;
                            result._promiseStatus = GPromise.FULFILLED;
                        }
                    } catch (e) {
                        //回調中拋出錯誤的情況
                        result._promiseStatus = GPromise.REJECTED;
                        result._promiseValue = e;
                    }
                }
            }, 0);
            //promise 之所以能夠鏈式操作,因爲返回了GPromise對象
            return result;
        }

測試用例

是騾子是馬,拉出來溜溜。。

測試環境是macOS Sierra 10.12.6,Chrome 60.0.3112.113。

經過以下測試, 證明了GPromise 的基本的異步流程管理和原生 Promise 沒有差別。以下測試用例參考了 MDN 中的Promise
API
中的 Advanced Example。

    var promiseCount = 0;

    function test(isPromise) {
        let thisPromiseCount = ++promiseCount,
            executor = (resolve, reject) => {
                console.log(thisPromiseCount + ') Promise started (Async code started)');
                window.setTimeout(
                    function () {
                        resolve(thisPromiseCount);
                    }, Math.random() * 2000 + 1000);
            };

        console.log(thisPromiseCount + ') Started (Sync code started)');

        let p1 = isPromise ? new Promise(executor) : new GPromise(executor);

        p1.then(
            function (val) {
                console.log(val + ') Promise fulfilled (Async code terminated)');
            },
            function (reason) {
                console.log('Handle rejected promise (' + reason + ') here.');
            });

        console.log(thisPromiseCount + ') Promise made (Sync code terminated)');
    }

    test();
    test(true);
    test();

那麼再來測試下鏈式操作(沒有鏈式操作的 Promise 我要你有何用?),測試結果和 Promise 表現一致。


    function async1() {
        return new GPromise(
            (resolve, reject) => {
                console.log('async1 start');
                setTimeout(() => {
                    resolve('async1 finished')
                }, 1000);
            }
        );
    }

    function async2() {
        return new GPromise(
            (resolve, reject) => {
                console.log('async2 start');
                setTimeout(() => {
                    resolve('async2 finished')
                }, 1000);
            }
        );
    }

    function async3() {
        return new GPromise(
            (resolve, reject) => {
                console.log('async3 start');
                setTimeout(() => {
                    resolve('async3 finished');
                }, 1000);
            }
        );
    }

    async1()
        .then(
            data => {
                console.log(data);
                return async2();
            })
        .then(
            data => {
                console.log(data);
                return async3();
            }
        )
        .then(
            data => {
                console.log(data);
            }
        );

總結

到此爲止,一個高仿的 Promise 已經實現完成了,它很簡單,因爲只有一個 then 方法,異步的狀態管理由內部完成。

這裏並沒有實現 catch方法,因爲上文也提到了,catch方法就相當於 then(null,onrejected) 。而且 Promise 類上的 race,all,resolve,reject也沒有實現,本文旨在理清 Promise 核心原理,篇幅受限(其實就是我懶),其他輔助類的方法等之後有時間再實現。

本文提供的只是一個思路,希望能幫助到你,歡迎大家批評指教。

代碼地址:Github

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章