Promise——從閱讀文檔到簡單實現(二)

前言

按照文檔說明簡單地實現 ES6 Promise的各個方法並不難,可是Promise的一些特殊需求實現起來並不簡單,我首先提出一些很差實現或者容易忽略的需求:git

  • 數據傳遞
  • 回調綁定
  • 將回調變成 microtask
  • 實現 then/finally 返回的pending promise 「跟隨」它們的回調返回的pending promise
  • 實現 resolve 返回的 promise 「跟隨」它的thenable對象參數

實現框架

在解決上述問題前,咱們先實現一個框架。
首先,個人目的是實現一個Promise插件,它包括:es6

  • 構造函數:Promise
  • 靜態方法:resolve、reject、all 和 race
  • 實例方法:then、catch 和 finally
  • 私有函數:identity、thrower 和 isSettled 等

以下:github

;(function() {
    function Promise(executor) {
    }
    
    Object.defineProperties(Promise, {
        resolve: {
            value: resolve,
            configurable: true,
            writable: true
        },
        reject: {
            value: reject,
            configurable: true,
            writable: true
        },
        race: {
            value: race,
            configurable: true,
            writable: true
        },
        all: {
            value: all,
            configurable: true,
            writable: true
        }
    });
    
    Promise.prototype = {
        constructor: Promise,
        
        then: function(onFulfilled, onRejected) {
        },
        
        catch: function(onRejected) {
        },
        
        finally: function(onFinally) {
        },
    }
    
    function identity(value) {
        return value;
    }
    
    function thrower(reason) {
        throw reason;
    }
    
    function isSettled(pro) {
        return pro instanceof Promise ? pro.status === 'fulfilled' || pro.status === 'rejected' : false;
    }
    
    window.Promise = Promise;
})();

解決問題

接下來,咱們解決各個問題。數組

數據傳遞

爲了傳遞數據——回調函數須要用到的參數以及 promise 的狀態,咱們首先在構造函數Promise中給新生成的對象添加statusvaluereason屬性,而且在構造函數中執行 executor 函數:promise

function Promise(executor) {
    var self = this;

    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;

    typeof executor === 'function' ? executor.call(null,
    function(value) {
        self.value = value;
        self.status = 'fulfilled';
    },
    function(reason) {
        self.reason = reason;
        self.status = 'rejected';
    }) : false;
}

咱們將 value、reason 和 status 保存在 Promise 對象中,這樣,咱們就能夠在 Promise 對象的方法中經過this(即 Promise 對象的引用)來訪問這些數據,並將其用做回調函數的參數。閉包

按照文檔說明,爲了實現鏈式調用,Promise的全部方法都會返回一個 Promise 對象,並且除了Promise.resolve(peomiseObj) 這種狀況外都是新生成的 Promise 對象。因此接下來個人大部分方法都會返回一個新的 promise 對象。不生成新對象的特例:框架

var a = Promise.resolve('a'),
    b = Promise.resolve(a);
console.log(a === b)    //true

回調綁定

接下來,咱們要將thencatchfinally中的回調方法綁定到Promise對象的狀態改變這個事件上。
我想到的第一個事件就是onchange事件,可是 promiseObj.status 屬性上並無change事件。可是,我立刻想到每次設置accessor屬性的值時,就會調用 accessor 屬性的setter方法。那麼,我只要把status屬性設置爲存取屬性,而後在它的 setter 方法裏觸發綁定的回調函數就行啦!以下:less

function Promise(executor) {
    var self = this;

    //存儲狀態的私有屬性
    this._status = 'pending';

    this.value = undefined;
    this.reason = undefined;
    //this.events = new Events();

    //存儲狀態的公開屬性
    Object.defineProperty(this, 'status', {
        get: function() {
            return self._status;
        },
        set: function(newValue) {
            self._status = newValue;
            //self.events.fireEvent('change');
        },
        configurable: true
    });

    typeof executor === 'function' ? executor.call(null,
    function(value) {
        self.value = value;
        self.status = 'fulfilled';
    },
    function(reason) {
        self.reason = reason;
        self.status = 'rejected';
    }) : false;
}

爲了綁定回調函數,我使用了發佈訂閱模式。在thencatchfinally方法執行的時候訂閱事件change,將本身的回調函數綁定到change事件上,promiseObj.status 屬性是發佈者,一旦它的值發生改變就發佈change事件,執行回調函數。
爲了節省篇幅,不那麼重要的發佈者Events() 構造函數及其原型我就不貼代碼了,文章末尾我會給出源代碼。異步

實現 microtask

thencatchfinally方法的回調函數都是microtask,當知足條件(promise 對象狀態改變)時,這些回調會被放入microtask隊列。每當調用棧中的macrotask執行完畢時,馬上執行microtask隊列中全部的microtask,這樣一次事件循環就結束了,js引擎等待下一次循環。
要我實現microtask我是作不到的,我就只能用macrotask模仿一下microtask了。
我用 setTimeout 發佈的macrotask進行模仿:ide

Object.defineProperty(this, 'status', {
    get: function() {
        return self._status;
    },
    set: function(newValue) {
        self._status = newValue;
        setTimeout(() => {
            self.events.fireEvent('change');
        },
        0);
    },
    configurable: true
});

實現函數

接下來,咱們實現各個函數和方法。在知道方法的參數和返回值後再實現方法若有神助,而實現過程當中最難處理的就是 pending 狀態的 promise 對象,由於咱們要等它變成其它狀態時,再作真正的處理。下面我拿出兩個最具表明性的方法來分析。

靜態方法all

若是忘記了 Promise.all(iterable) 的參數和返回值,能夠返回我上一篇文章查看。

function all(iterable) {
    //若是 iterable 不是一個可迭代對象
    if (iterable[Symbol.iterator] == undefined) {
        let err = new TypeError(typeof iterable + iterable + ' is not iterable (cannot read property Symbol(Symbol.iterator))');
        return Promise.reject(err);
    }

    //若是 iterable 對象爲空
    if (iterable.length === 0) {
        return Promise.resolve([]);
    }

    //其它狀況用異步處理
    var pro = new Promise(),    //all 返回的 promise 對象
        valueArr = [];         //all 返回的 promise 對象的 value 屬性
    setTimeout(function() {
        var index = 0,     //記錄當前索引
        count = 0,
        len = iterable.length;

        for (let val of iterable) { -
            function(i) {
                if (val instanceof Promise) { //當前值爲 Promise 對象時
                    if (val.status === 'pending') {
                        val.then(function(value) {
                            valueArr[i] = value;
                            count++;
                            //Promise.all([new Promise(function(resolve){setTimeout(resolve, 100, 1)}), 2, 3, 4])
                            if (count === len) {
                                pro.value = valueArr;
                                pro.status = 'fulfilled';
                            }
                        },
                        function(reason) {
                            pro.reason = reason;
                            pro.status = 'rejected';
                            //當一個pending Promise首先完成時,解除其它 pending Promise的事件,防止以後其它 Promise 改變 pro 的狀態
                            for (let uselessPromise of iterable) {
                                if (uselessPromise instanceof Promise && uselessPromise.status === 'pending') {
                                    uselessPromise.events.removeEvent('change');
                                }
                            }
                        });
                    } else if (val.status === 'rejected') {
                        pro.reason = val.reason;
                        pro.status = 'rejected';
                        return;
                    } else {
                        //val.status === 'fulfilled'
                        valueArr[i] = val.value;
                        count++;
                    }
                } else {
                    valueArr[i] = val;
                    count++;
                }
                index++;
            } (index);
        }

        //若是 iterable 對象中的 promise 對象都變爲 fulfilled 狀態,或者 iterable 對象內沒有 promise 對象,
        //因爲咱們可能須要等待 pending promise 的結果,因此要額外花費一個變量計數,而不能用valueArr的長度判斷。
        if (count === len) {
            pro.value = valueArr;
            pro.status = 'fulfilled';
        }
    },
    0);

    return pro;
}

這裏解釋兩點:

一、如何保證 value 數組中值的順序
若是iterable對象中的 promise 對象都變爲 fulfilled 狀態,或者 iterable 對象內沒有 promise 對象,all 返回一個 fulfilled promise 對象,且其 value 值爲 iterable 中各項值組成的數組,數組中的值將會按照 iterable 內的順序排列,而不是由 pending promise 的完成順序決定。
爲了保證 value 數組中值的順序,最簡單的方法是

valueArr[iterable.indexOf(val)] = val.value;

可是像除 Array、TypedArray 和 String 外的 Map 和 Set 原生 iterabe 對象,以及其它經過myIterable[Symbol.iterator] 建立的自定義的 iterable 對象都沒有 indexOf 方法,因此我選擇用閉包來保證 value 數組值的順序。

二、處理 pending promise 對象。
pending promise 是致使這個函數要額外添加不少變量存儲狀態,額外作不少判斷和處理的罪魁禍首。
若是 iterabe 對象中有一個pending狀態的 promise(一般爲一個異步的 promise),咱們就使用then方法來持續關注它的動態。

  • 一旦它變成fulfilledpromise,就將它的 value 加入 valueArr 數組。咱們添加一個 count 變量記錄目前 valueArr 獲取到了多少個值,當所有獲取到值後,就能夠給 pro.value 和pro.status 賦值了。之因此用 count 而不是 valueArr.length 判斷,是由於 valueArr = [undefined,undefined,undefined,1] 的長度也爲4,這樣可能致使還沒獲取到 pending promise 的值就改變 pro.status 了。
  • 而當它變成rejectedpromise 時,咱們就更新 all 方法返回的對象的 reason 值,同時改變狀態 status 爲 rejected,觸發綁定的onrejected函數。另外,爲了與原生 Promise 表現相同:若是 iterable 對象中任意一個 pending promise 對象狀態變爲 rejected,將再也不持續關注其它 pending promise 的動態。而我早就在全部的 pending promise 上都綁定了 onfulfilled 和 onrejected 函數,用來跟蹤它們。因此我須要在某個 pending promise 變爲 rejected promise 時,刪除它們綁定的回調函數。

實例方法then

Promise.prototype.then(onFulfilled, onRejected):

Promise.prototype.then = function(onFulfilled, onRejected) {
    var pro = new Promise();

    //綁定回調函數,onFulfilled 和 onRejected 用一個回調函數處理
    this.events.addEvent('change', hander.bind(null, this));

    function hander(that) {
        var res; //onFulfilled 或 onRejected 回調函數執行後獲得的結果
        try {
            if (that.status === 'fulfilled') {
                //若是onFulfilled不是函數,它會在then方法內部被替換成一個 Identity 函數
                typeof onFulfilled !== 'function' ? onFulfilled = identity: false;
                //將參數 this.value 傳入 onFulfilled 並執行,將結果賦給 res
                res = onFulfilled.call(null, that.value);
            } else if (that.status === 'rejected') {
                //若是onRejected不是函數,它會在then方法內部被替換成一個 Thrower 函數
                typeof onRejected !== 'function' ? onRejected = thrower: false;

                res = onRejected.call(null, that.reason);
            }
        } catch(err) {
            //拋出一個錯誤,狀況3
            pro.reason = err;
            pro.status = 'rejected';

            return;
        }

        if (res instanceof Promise) {
            if (res.status === 'fulfilled') {            //狀況4
                pro.value = res.value;
                pro.status = 'fulfilled';
            } else if (res.status === 'rejected') {      //狀況5
                pro.reason = res.reason;
                pro.status = 'rejected';
            } else {                                     //狀況6
                //res.status === 'pending'時,pro 跟隨 res
                pro.status = 'pending';
                res.then(function(value) {
                    pro.value = value;
                    pro.status = 'fulfilled';
                },
                function(reason) {
                    pro.reason = reason;
                    pro.status = 'rejected';
                });
            }
        } else {
            //回調函數返回一個值或不返回任何內容,狀況一、2
            pro.value = res;
            pro.status = 'fulfilled';
        }
    }

    return pro;
};

我想我已經註釋得很清楚了,能夠對照我上一篇文章進行閱讀。
我再說明一下pending promise 的「跟隨」狀況,和 all 方法的實現方式差很少,這裏也是用 res.then來「跟隨」的。我相信你們都看得懂代碼,下面我舉個例子來實踐一下:

var fromCallback;

var fromThen = Promise.resolve('done')
.then(function onFulfilled(value) {
    fromCallback = new Promise(function(resolve){
        setTimeout(() => resolve(value), 0);    //未執行 setTimeout 的回調方法以前 fromCallback 爲'pending'狀態
    });
    return fromCallback;    //then 方法返回的 fromThen 將跟隨 onFulfilled 方法返回的 fromCallback
});

setTimeout(function() {
    //目前已執行完 onFulfilled 回調函數,fromCallback 爲'pending'狀態,fromThen ‘跟隨’ fromCallback
    console.log(fromCallback.status);    //fromCallback.status === 'pending'
    console.log(fromThen.status);        //fromThen.status === 'pending'
    setTimeout(function() {
        //目前已執行完 setTimeout 中的回調函數,fromCallback 爲'fulfilled'狀態,fromThen 也跟着變爲'fulfilled'狀態
        console.log(fromCallback.status + ' ' + fromCallback.value);    //fromCallback.status === 'fulfilled'
        console.log(fromThen.status + ' ' + fromThen.value);        //fromThen.status === 'fulfilled'
        console.log(fromCallback === fromThen);        //false
    }, 10);    //將這個 delay 參數改成 0 試試
}, 0);

看完這個例子,我相信你們都搞懂了then的回調函數返回 pending promise 時它會怎麼處理了。
另外,這個例子也體現出我用 setTimeout 分發的macrotask模擬microtask的不足之處了,若是將倒數第二行的的 delay 參數改成 0,那麼 fromThen.status === 'pending',這說明修改它狀態的代碼在 log 它狀態的代碼以後執行,至於緣由你們本身想一下,這涉及到 event loop。

測試

各位大俠請點下面的連接進行測試:
https://codepen.io/lyl123321/...

或者直接點這裏查看源代碼:
https://github.com/lyl123321/...
新增 Promise.try:
https://github.com/lyl123321/...

相關文章
相關標籤/搜索