# 实现一个完全符合规范的Promise

从网上看了太多名为《手把手教你写个Promise》的教程,内容大同小异。 几乎都有一个缺点,没有把微任务加上,都是用setTimeout处理的异步,虽然Promise A+规范 (opens new window)并没有要求这个,但像浏览器、nodejs都把Promise当作微任务中的一种,所以我们需要模拟下。

此节暂时按下不表。

我们一点点来。

# 简单实现

先看下promise的例子:

aa = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'ok');
});
aa.then((res) => console.log(res));

bb = new Promise((resolve, reject) => {
    setTimeout(reject, 100, 'error');
});
bb.catch((err) => console.error(err));

Promise.resolve(123).then(function(){
    return Promise.resolve(456);
}).then(function(res){
    console.log(res); //打印456
});

一般来说,我们只需要知道以下几点,就差不多了。

  1. Promise构造函数的参数是个函数,这个函数又显式地要求有2个参数resolvereject方法,分别对应成功或者失败的函数

  2. 每个Promise实例是可以用.then或者.catch链式调用,而每次调用,其实都不是它自己了,都会返回一个新的Promise实例

  3. .then方法有2个函数作为参数,分别对应成功处理和失败处理

  4. 一个实例有3种状态(进行中(pending)、成功(fulfilled)、失败(rejected)),成功或失败后就不能再更改状态了

参见以下这种情况,先resolve了,但后面发生异常,依然是成功状态。

new Promise((resolve, reject) => {
      resolve('111');       
      throw new Error('err');
  })
}.then(function(){
  console.log('--ok--'); //打印此句
}).catch(function(){
  console.log('-------err--')
});

其细节颇多,要完整符合规范并不容易,我这里也先写个简单的。

在写之前,我们需要思考下难点在哪里。

我们知道,Promise的出现,是为了解决前端异步回调的痛点,它的每次.then,都是异步的操作。但在js里,我们.then的时候,其实是执行了一个对象的then方法,这是个同步的操作。

也就是说,then方法中的代码,肯定不能立即执行,必须得缓存起来,在合适的时机再执行它。

而什么时候执行呢?代码中显式调用resolvereject以后,就可以执行了。

一般来说,有2种延时方案: 一种是在resolve或者reject执行时,进行延时; 一种是在.then方法里,执行对应的函数时进行延时。

2种都能实现大致功能,但前者在完全符合规范编写过程中遇坑不少,这里我们选择第二种。

# 步骤一:初始化构造函数

先定义3种状态:

const PENDING = 'pending'; //进行中
const FULFILLED = 'fulfilled'; //成功
const REJECTED = 'rejected'; //失败

再写构造函数,内置3个属性——状态status、成功返回值value、失败原因reason。把resolvereject这两个内置函数作为fn的参数执行,它俩的责任就是改变当前promise的状态值。

class Promise {
    constructor(fn) {
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined;
        const resolve = (val) => {
            if (this.status !== PENDING) { // 参见第4条,状态变化后就不能再修改了
                return;
            }
            this.status = FULFILLED;
            this.value = val;
            //todo
        };

        const reject = (err) => {
            if (this.status !== PENDING) { // 参见第4条,状态变化后就不能再修改了。
                return;
            }
            this.status = REJECTED;
            this.reason = err;
            //todo
        };
        try {
            fn(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }
}

# 步骤二:实现then方法

从第2条和第3条可知,then方法需要返回一个新的Promise,且要异步处理。怎么异步处理呢?上面提到,需要把then的这两个参数缓存起来,于是我们把它们分别存到2个数组中。

为什么用数组,而不是普通变量呢?我开始也困惑了好久,后来想到,有这样一种使用场景:

a = Promise.resolve(123);
a.then(console.log);
a.then(console.error);

上述一个promise会在不同的地方.then执行,显然,两个回调函数应该一前一后执行,而不是丢失某一个。

所以,我们在构造函数中添加2个数组:

 constructor(fn) {
    ...
    this.onFulfilledList = [];
    this.onRejectedList = [];
    ...
 }

继而在then中,在状态为pending时,把需要延时处理的函数推送到这俩数组中。

需要注意的是,对于then传递的这俩参数,如果在需要的时候它不是函数,则会忽略不计,把当前promise的状态传递到下一个。

then(onFulfilled, onRejected) {
    return new Promise((resolve, reject) => {
        const onResolvedFunc = function (val) {
            const cb = function () {
                try {
                    if (typeof onFulfilled !== 'function') { // 如果成功了,它不是个函数,意味着不能处理,则把当前Promise的状态继续向后传递
                        resolve(val);
                        return;
                    }
                    const x = onFulfilled(val);
                    resolve(x);
                } catch (e) {
                    reject(e);
                }
            };
            setTimeout(cb, 0);
        };

        const onRejectedFunc = function (err) {
            const cb = function () {
                try {
                    if (typeof onRejected !== 'function') { // 如果失败了,它不是个函数,意味着不能处理,则把当前Promise的状态继续向后传递
                        reject(err);
                        return;
                    }
                    const x = onRejected(err);
                    resolve(x); //处理了失败,则意味着要返回的新的promise状态是成功的
                } catch (e) {
                    reject(e);
                }
            };
            setTimeout(cb, 0);
        };

        if (this.status === PENDING) {
            this.onFulfilledList.push(onResolvedFunc);
            this.onRejectedList.push(onRejectedFunc);
        } else if (this.status === FULFILLED) {
            //todo
        } else {
            //todo
        }
    });
}

下来就是在构造函数的resolvereject中执行我们缓存的函数。

constructor(fn) {
    ...
    const resolve = (val) => {
        ...
        this.onFulfilledList.forEach((cb) => cb && cb.call(this, val));
        this.onFulfilledList = [];
    };

    const reject = (err) => {
        ...
        this.onRejectedList.forEach((cb) => cb && cb.call(this, err));
        this.onRejectedList = [];
    };
    ...
}

现在,下面的例子应该可以成功了:

new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('111');
    }, 0);
}).then(function () {
    console.log('--ok--')
});

但去掉延时(setTimeout)就会失败。分析下原因就明白了,我们在调用resolve时就改变了status,走到then方法时,状态已经变为成功了。所以我们需要在状态已经变为成功或失败时,直接调用回调函数,而不是依赖resolvereject触发:

then(onFulfilled, onRejected) {
    ...
    if (this.status === PENDING) {
        ...
    } else if (this.status === FULFILLED) { 
        onResolvedFunc(this.value);
    } else { // if(this.status === REJECTED) { //如果这个Promise已经失败,说明已经reject过了,不能再依赖reject来触发,就直接执行失败处理。
        onRejectedFunc(this.reason);
    }
}

这样,下面的代码就ok了:

new Promise((resolve, reject) => {
    resolve('111');
}).then(function () {
    console.log('--ok--')
});

# 步骤三:实现catch

catch的实现很简单,就是个then的语法糖:

catch(onRejected) {
    return this.then(null, onRejected);
}

# 步骤四:模拟微任务

按理说promise的主要功能已经实现了,但下面的代码就会暴露一个问题:

setTimeout(function(){
    console.log('---timeout--')
});
new Promise((resolve, reject) => {
    resolve('111');
}).then(function () {
    console.log('--ok--')
});
console.log('--log--');   

会先打印log,这是正常的,再打印timeout,最后打印ok。这就违反了微任务优先的原则,毕竟它是VIP啊。

怎么做呢?

浏览器中有个MutationObservernodejs中可以使用process.nextTick,这俩都没有的时候,再回退到setTimeout。这才是正确的实现姿势。

为了图方便,我照抄了vuenextTick中的代码,具体可见这里

const isInBrowser = typeof window !== 'undefined';
const nextTick = function(nextTickHandler) {
    if (isInBrowser) {
        if (typeof MutationObserver !== 'undefined') { // 首选 MutationObserver 
            var counter = 1;
            var observer = new MutationObserver(nextTickHandler); // 声明 MO 和回调函数
            var textNode = document.createTextNode(counter);
            observer.observe(textNode, { // 监听 textNode 这个文本节点
                characterData: true // 一旦文本改变则触发回调函数 nextTickHandler
            });
            const start = function () {
                counter = (counter + 1) % 2; // 每次执行 timeFunc 都会让文本在 1 和 0 间切换
                textNode.data = counter;
            };
            start();
        } else {
            setTimeout(nextTickHandler, 0);
        }
    } else {
        process.nextTick(nextTickHandler);
    }
};

再把then方法中setTimeout换成nextTick就可以了。

# 完整实现

怎么能完全符合规范呢?有个检查工具:promises-aplus-tests, 在npm中安装一下,再在script标签中使用:

"test": "promises-aplus-tests src/promise.js --reporter spec"

需要你的promise.jsmodule.exports导出,再在内部实现一个deferred方法:

static deferred() {
    let result = {};
    result.promise = new Promise((resolve, reject) => {
        result.resolve = resolve;
        result.reject = reject;
    });
    return result;
}

接下来就能执行所有的测试用例了。

还有些额外的方法实现,不在这里展现了,可以看这篇

具体细节就不再补充了。以下是完整代码实现:

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
const isInBrowser = typeof window !== 'undefined';
class Promise {
    static isPromise(val) {
        return val && val instanceof Promise;
    }

    static resolve(val) {
        if (Promise.isPromise(val)) {
            return val;
        }
        return new Promise((resolve) => {
            if (val && typeof val.then === 'function') { // 如果返回值是个thenable对象,需要处理下
                val.then((res) => {
                    resolve(res);
                });
                return;
            }
            resolve(val);
        });
    }

    static reject(val) {
        //reject不区分是不是Promise
        return new Promise((_, reject) => {
            reject(val);
        });
    }

    /**
     * 用MutationObserver生成浏览器的nextTick,nodejs端则用process.nextTick
     */
    static nextTick(nextTickHandler) {
        if (isInBrowser) {
            if (typeof MutationObserver !== 'undefined') { // 首选 MutationObserver 
                var counter = 1;
                var observer = new MutationObserver(nextTickHandler); // 声明 MO 和回调函数
                var textNode = document.createTextNode(counter);
                observer.observe(textNode, { // 监听 textNode 这个文本节点
                    characterData: true // 一旦文本改变则触发回调函数 nextTickHandler
                });
                const start = function () {
                    counter = (counter + 1) % 2; // 每次执行 timeFunc 都会让文本在 1 和 0 间切换
                    textNode.data = counter;
                };
                start();
            } else {
                setTimeout(nextTickHandler, 0);
            }
        } else {
            process.nextTick(nextTickHandler);
        }
    }

    static all(arr) {
        if (!Array.isArray(arr)) {
            throw new TypeError('undefined is not iterable.');
        }
        let count = arr.length;
        const result = [];
        if (count === 0) {
            return Promise.resolve(result);
        }
        return new Promise((resolve, reject) => {
            Promise.resolve(promise).then((res) => {
                count--;
                result[i] = res;
                if (count === 0) {
                    resolve(result);
                }
            }, reject);
        });
    }

    static allSettled(arr) {
        if (!Array.isArray(arr)) {
            throw new TypeError('undefined is not iterable.');
        }
        let count = arr.length;
        const result = [];
        if (count === 0) {
            return Promise.resolve(result);
        }
        return new Promise((resolve) => {
            arr.forEach((promise, i) => {
                Promise.resolve(promise).then((res) => {
                    count--;
                    result[i] = {
                        value: res,
                        status: FULFILLED
                    };
                    if (count === 0) {
                        resolve(result);
                    }
                }, (err) => {
                    count--;
                    result[i] = {
                        reason: err,
                        status: REJECTED
                    };
                    if (count === 0) {
                        resolve(result);
                    }
                });
            });
        });
    }

    static race(arr) {
        if (!Array.isArray(arr)) {
            throw new TypeError('undefined is not iterable.');
        }
        let count = arr.length;
        if (count === 0) {
            return new Promise(() => { }); //返回一个永远pending的promise,这是跟all等不一样的地方
        }
        return new Promise((resolve, reject) => {
            arr.forEach((promise) => {
                Promise.resolve(promise).then(resolve, reject);
            });
        });
    }

    static deferred() {
        let result = {};
        result.promise = new Promise((resolve, reject) => {
            result.resolve = resolve;
            result.reject = reject;
        });
        return result;
    }

    /**
     * 先执行同步代码,替代Promise.resolve().then(func)
     * @example 
     * 
     * const f = () => console.log('now');
     * Promise.try(f);
     * console.log('next');
     */
    static try(func){
        return new Promise((resolve) => {
            resolve(func());
        });
    }

    constructor(fn) {
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined;

        this.onFulfilledList = [];
        this.onRejectedList = [];
        try {
            fn(this.handleResolve.bind(this), this.handleReject.bind(this));
        } catch (e) {
            this.handleReject(e);
        }
    }

    /**
     * 处理成功
     * 就是我们使用的resolve函数
     * @param val 成功参数
     */
    handleResolve(val) {
        if (this.status !== PENDING) {
            return;
        }
        this.status = FULFILLED;
        this.value = val;
        this.onFulfilledList.forEach((cb) => cb && cb.call(this, val));
        this.onFulfilledList = [];
    }

    /**
     * 处理失败
     * 就是我们使用的reject函数
     * @param err 失败信息
     */
    handleReject(err) {
        if (this.status !== PENDING) {
            return;
        }
        this.status = REJECTED;
        this.reason = err;
        this.onRejectedList.forEach((cb) => cb && cb.call(this, err));
        this.onRejectedList = [];
    }

    then(onFulfilled, onRejected) {
        const promise2 = new Promise((resolve, reject) => {
            const resolvePromise = function (x) {
                if (x === promise2) {
                    reject(new TypeError('The promise and the return value are the same'));
                    return;
                }
                if (x && typeof x === 'object' || typeof x === 'function') {
                    let used; //PromiseA+2.3.3.3.3 只能调用一次
                    try {
                        let then = x.then;
                        if (typeof then === 'function') {
                            //PromiseA+2.3.3
                            then.call(x, (y) => {
                                //PromiseA+2.3.3.1
                                if (used) return;
                                used = true;
                                resolvePromise(y);
                            }, (r) => {
                                //PromiseA+2.3.3.2
                                if (used) return;
                                used = true;
                                reject(r);
                            });
                        } else {
                            //PromiseA+2.3.3.4
                            if (used) return;
                            used = true;
                            resolve(x);
                        }
                    } catch (e) {
                        //PromiseA+ 2.3.3.2
                        if (used) return;
                        used = true;
                        reject(e);
                    }
                } else {
                    //PromiseA+ 2.3.3.4
                    resolve(x);
                }
            };

            const onResolvedFunc = function (val) {
                var cb = function () {
                    try {
                        if (typeof onFulfilled !== 'function') { // 如果成功了,它不是个函数,意味着不能处理,则把当前Promise的状态继续向后传递
                            resolve(val);
                            return;
                        }
                        const x = onFulfilled(val);
                        resolvePromise(x);
                    } catch (e) {
                        reject(e);
                    }
                };
                Promise.nextTick(cb);
            };

            const onRejectedFunc = function (val) {
                var cb = function () {
                    try {
                        if (typeof onRejected !== 'function') { // 如果失败了,它不是个函数,意味着不能处理,则把当前Promise的状态继续向后传递
                            reject(val);
                            return;
                        }
                        const x = onRejected(val);
                        resolvePromise(x);
                    } catch (e) {
                        reject(e);
                    }
                };
                Promise.nextTick(cb);
            };

            if (this.status === PENDING) {
                //这样把then注册的函数,放到list中延时执行。内部加了try/catch,把修改状态的逻辑全放在了handleResolve、handleReject这俩函数中
                this.onFulfilledList.push(onResolvedFunc);
                this.onRejectedList.push(onRejectedFunc);
            } else if (this.status === FULFILLED) { //如果这个Promise已经成功,说明已经resolve过了,不能再依赖resolve来触发,就直接执行成功处理。比如aa = Promise.resolve(),有多处使用.then
                onResolvedFunc(this.value);
            } else { // if(this.status === REJECTED) { //如果这个Promise已经失败,说明已经reject过了,不能再依赖reject来触发,就直接执行失败处理。
                onRejectedFunc(this.reason);
            }
        });

        return promise2;
    }

    catch(onRejected) {
        return this.then(null, onRejected);
    }

    finally(callback) {
        return this.then(
            (val) => Promise.resolve(callback()).then(() => val),
            (err) => Promise.resolve(callback()).then(() => { throw err })
        );
    }

    done(onFulfilled, onRejected) {
        this.then(onFulfilled, onRejected)
            .catch(function (reason) {
                // 抛出一个全局错误
                setTimeout(() => {
                    throw reason
                }, 0);
            });
    }

    toString() {
        return '[object Promise]';
    }
}

module.exports = Promise;