1. 程式人生 > >解析 Promise 原理,實現一個Promise

解析 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 方法,這個方法也是實現非同步的鏈式操作的基本。

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 物件。(下文會具體分析)

動手實現

做了上面的鋪墊,實現一個 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 核心原理,篇幅受限(其實就是我懶),其他輔助類的方法等之後有時間再實現。

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