手寫一個符合A+規範的Promise

本文同時也發佈在我的github博客上,歡迎star~

之前也手寫過簡單的promise,這次則是爲了通過官方的Promise A+測試集,借鑑了一些下載量較多的promise polyfill,改了幾遍,終於是通過了A+規範的872個測試用例

如何測試?

測試庫地址在這:promises-tests ,大家在寫完自己的promise後,不妨也去測試一下,檢驗自己的promise是否符合Promise A+規範。這個庫使用起來很方便,像下面這樣就可以了:

const tests = require("promises-aplus-tests");
const Promise = require("./index");

const deferred = function() {
    let resolve, reject;
    const promise = new Promise(function(_resolve, _reject) {
        resolve = _resolve;
        reject = _reject;
    });
    return {
        promise: promise,
        resolve: resolve,
        reject: reject
    };
};
const adapter = {
    deferred
};
tests.mocha(adapter);

其中,index.js中是你寫的Promise

實現

首先我們定義一些全局屬性:

const IS_ERROR = {};
let ERROR = null;

IS_ERROR作爲發生錯誤時的標識,ERROR用來保存錯誤;

做好準備工作,再來定義_Promise類,其中fn是Promise接受的函數,構造函數執行時立刻調用;_status是Promise的狀態,初始爲0(pending),resolved時爲1,rejected時爲2;_value用來保存Promise resolved時的返回值和rejected時的失敗信息;_handlers用來保存Promise成功和失敗時調用的處理方法

function _Promise(fn) {
    this._status = 0;
    this._value = null;
    this._handlers = [];
    doFn(this, fn);
}

最後執行doFn方法,傳入this值和fn:

function doFn(self, fn) {
    const ret = safeCallTwo(
        fn,
        function(value) {
            self.resolve(value);
        },
        function(reason) {
            self.reject(reason);
        }
    );
    if (ret === IS_ERROR) {
        self.reject(ERROR);
    }
}

其中safeCallTwo是用來安全執行兩參數方法的函數,當執行出錯時,捕獲錯誤,保存在ERROR中,返回IS_ERROR標識:

function safeCallTwo(fn, arg1, arg2) {
    try {
        return fn(arg1, arg2);
    } catch (error) {
        ERROR = error;
        return IS_ERROR;
    }
}

doFn中,調用safeCallTwo,fn傳入兩個參數供我們調用,也就是我們常用的resolve方法和reject方法,並獲取到返回值,如果ret爲錯誤標識IS_ERROR,則調用reject

_Promise原型上掛載着resolve和reject方法,如下:

_Promise.prototype.resolve = function(value) {
    if (this._status !== 0) {
        return;
    }
    this._status = 1;
    this._value = value;
    doThen(this);
};
_Promise.prototype.reject = function(reason) {
    if (this._status !== 0) {
        return;
    }
    this._status = 2;
    this._value = reason;
    doThen(this);
};

因爲Promise的狀態只能由pending轉爲resolvedrejected,所以在執行resolve和reject方法時,要先判斷status是否爲0,若不爲0,直接return;修改status和value後,執行doThen方法:

function doThen(self) {
    const handlers = self._handlers;
    handlers.forEach(handler => {
        doHandler(self, handler);
    });
}

doThen函數的作用是從self上取出的handlers並依次執行

我們再來看一看掛載在原型上的then方法:

_Promise.prototype.then = function(onResolve, onReject) {
    const res = new _Promise(function() {});
    preThen(this, onResolve, onReject, res);
    return res;
};

我們知道,Promise是支持鏈式調用的,所以我們的then方法也會返回一個Promise,以供後續調用;

下面是preThen方法:

function preThen(self, onResolve, onReject, res) {
    onResolve = typeof onResolve === "function" ? onResolve : null;
    onReject = typeof onReject === "function" ? onReject : null;
    const handler = {
        onResolve,
        onReject,
        promise: res
    };
    if (self._status === 0) {
        self._handlers.push(handler);
        return;
    }
    doHandler(self, handler);
}

preThen方法接受4個值,分別爲當前Promise——self,resolve後的回調函數onResolve,reject後的回調函數onReject,then函數返回的promise——res。先判斷onResolve和onReject是否爲函數,若不是,直接置爲null。再將onResolve、onReject、res放入handler對象中

接下來需要注意,Promise接受的函數(也就是上文的fn)並不是一定是異步調用resolve和reject,也有可能是同步的,也就是說在執行preThen函數時,self的status可能已經不爲0了,這時候我們就不需要將handler保存起來等待調用,而是直接調用回調函數

doHandler函數代碼見下:

function doHandler(self, handler) {
    setTimeout(() => {
        const { onReject, onResolve, promise } = handler;
        const { _status, _value } = self;
        const handlerFun = _status === 1 ? onResolve : onReject;
        if (handlerFun === null) {
            _status === 1 ? promise.resolve(_value) : promise.reject(_value);
            return;
        }
        const ret = safeCallOne(handlerFun, _value);
        if (ret === IS_ERROR) {
            promise.reject(ERROR);
            return;
        }
        promise.resolve(ret);
    });
}

我們知道,即使是同步執行relove或者reject,then函數接受的回調函數也不會立刻同步執行,如下代碼會依次輸出1,3,2,而非1,2,3

const p = new Promise(resolve => {
    console.log(1);
    resolve();
});
p.then(() => {
    console.log(2);
});
console.log(3);

在這裏,我使用了setTimeout來模擬這種模式,當然,這只是一種粗糙的模擬,更好的方式是引入或實現類似asap的庫(下個星期我可能會實現這個,哈哈),但setTimeout也足夠通過測試了

doHandler函數中,我們調用相應的回調函數,需要注意的是,如果相應回調函數爲null(null是前文判斷回調函數不爲function時統一賦值的),則直接調用then函數返回的promise的resolve或reject方法。

同樣,我們使用了safeCallOne來捕獲錯誤,這裏不再贅述

到這裏,我們執行測試,發現不出意外地沒有通過,因爲我們只是實現了基礎的Promise,還沒有實現resolve中的thenable功能,下面是mdn對於thenable的描述:

返回一個狀態由給定value決定的Promise對象。如果該值是thenable(即,帶有then方法的對象),返回的Promise對象的最終狀態由then方法執行決定;否則的話(該value爲空,基本類型或者不帶then方法的對象),返回的Promise對象狀態爲fulfilled,並且將該value傳遞給對應的then方法。通常而言,如果你不知道一個值是否是Promise對象,使用Promise.resolve(value) 來返回一個Promise對象,這樣就能將該value以Promise對象形式使用

我們再來修改resolve方法:

_Promise.prototype.resolve = function(value) {
    if (this._status !== 0) {
        return;
    }
    if (this === value) {
        return this.reject(new TypeError("cant's resolve itself"));
    }
    if (value && (typeof value === "function" || typeof value === "object")) {
        const then = getThen(value);
        if (then === IS_ERROR) {
            this.reject(ERROR);
            return;
        }
        if (value instanceof _Promise) {
            value.then(
                value => {
                    this.resolve(value);
                },
                reason => {
                    this.reject(reason);
                }
            );
            return;
        }
        if (typeof then === "function") {
            doFn(this, then.bind(value));
            return;
        }
    }
    this._status = 1;
    this._value = value;
    doThen(this);
};

先判斷this和value是否爲一個Promise,若是一個,則拋出錯誤

再判斷value的類型是否爲function或object,如果是,則實行getThen方法進行錯誤捕獲:

function getThen(self) {
    try {
        return self.then;
    } catch (error) {
        ERROR = error;
        return IS_ERROR;
    }
}

若成功拿到then方法,檢測value instanceof _Promise,若爲true,則直接採用value的狀態和value或者reason。

若then爲function,則將then函數以value爲this值,當作fn執行,也就是達成下面代碼的效果:

const p = new Promise(resolve => {
    resolve({
        then: _resolve => {
            _resolve(1);
        }
    });
});
p.then(value => console.log(value)); //打印1

我們再次執行測試,發現仍然有錯,其因出現在下面這種情況下:

const p = new _Promise(resolve => {
    resolve({
        then: _resolve => {
            setTimeout(() => _resolve(1)), 500;
        }
    });
    resolve(2);
});
p.then(value => console.log(value));

這個時候,使用我們的Promise,輸出的是2,而在規範中,應當是輸出1

原因是我們在對象的then方法中是異步地resolve,這個時候,下面的resolve(2)在執行時,status還沒有變,自然可以修改status和value

解決方法也很簡單,只用在doFn方法中判斷是否爲第一次執行即可:

function doFn(self, fn) {
    let done = false;
    const ret = safeCallTwo(
        fn,
        function(value) {
            if (done) {
                return;
            }
            done = true;
            self.resolve(value);
        },
        function(reason) {
            if (done) {
                return;
            }
            done = true;
            self.reject(reason);
        }
    );
    if (ret === IS_ERROR) {
        if (done) {
            return;
        }
        done = true;
        self.reject(ERROR);
    }
}

再執行測試,發現已經測試用例全部通過~
4BCA9C53-AC78-4BC4-880B-9FAB820A3074

代碼

完整代碼已放在我的github上,地址爲https://github.com/Bowen7/playground/tree/master/promise-polyfill ,可以clone我的playground項目,再到promise-polyfill目錄下npm install,然後執行npm test即可運行測試

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