原生es5封装的Promise对象

前言

前一阵看了一些关于JS异步操作的文章,发现Promise真是个好东西,配合Generator或者async/await使用更有奇效。完美解决异步代码书写的回调问题,有助于书写更优雅的异步代码。花了几天时间研究了Promise的工作机制,手痒痒用es6语法封装了一个Promise对象,基本实现了原生Promise的功能,现在,用es5语法再写一遍。


更新说明

  • 更新时间:2019/1/23

@logbn520兄弟的提醒,我把then方法的执行做成同步的了,是不符合规范的。

《Promises/A+规范》中,【Then 方法】小节【调用时机】部分写道:“onFulfilled 和 onRejected 只有在执行环境堆栈仅包含平台代码时才可被调用”,这里特别要看一下注释。

因此我要把onFulfilledonRejected 的代码放在“ then 方法被调用的那一轮事件循环之后的新执行栈中执行”,通过setTimeout方法将任务放到本轮任务队列的末尾。代码已添加到最后一部分-第九步。

关于任务队列的运行机制,感兴趣可看一下阮一峰老师的《JavaScript 运行机制详解:再谈Event Loop》


实现功能:

  • 已实现 Promise 基本功能,与原生一样,异步、同步操作均ok,具体包括:
    • MyPromise.prototype.then()
    • MyPromise.prototype.catch() 与原生 Promise 略有出入
    • MyPromise.prototype.finally()
    • MyPromise.all()
    • MyPromise.race()
    • MyPromise.resolve()
    • MyPromise.reject()
  • rejected 状态的冒泡处理也已解决,当前Promise的reject如果没有捕获,会一直冒泡到最后,直到catch
  • MyPromise 状态一旦改变,将不能再改变它的状态

不足之处:

  • 代码的错误被catch捕获时,提示的信息(捕获的错误对象)比原生Promise要多

测试: index.html

  • 这个页面中包含了30个测试例子,分别测试了各项功能、各个方法,还有一些特殊情况测试;或许还有有遗漏的,感兴趣自己可以玩一下;
  • 更加友好的可视化的操作,方便测试,每次运行一个例子,右边面板可看到结果;
  • 自定义了console.mylog()方法用来输出结果,第一个参数是当前使用的Promise对象,用以区分输出,查看代码时可忽略,后面的参数都是输出结果,与系统console.log()相似;
  • 建议同时打开 index.js 边看代码边玩;
  • 同一套代码,上面的 MyPromise 的运行结果,下面是原生 Promise 运行的结果;

收获

  • 再写一遍又有新收获,写的更顺了,理解更深刻了;
  • then/catch方法是最难的,要不停地修修补补;
  • reject状态的冒泡是个难题,但在下面的代码中我没有专门提及,我也没有办法具体说清楚他,我是在整个过程中不停地调才最终调出来正确的冒泡结果。

代码

下面贴代码,包括整个思考过程,会有点长
为了说明书写的逻辑,我使用以下几个注释标识,整坨变动的代码只标识这一坨的开头处。
//++ ——添加的代码
//-+ ——修改的代码

第一步,基础功能实现

名字随便取,我的叫MyPromise,没有取代原生的Promise。

  • 构造函数传入回调函数 callback 。当新建 MyPromise 对象时,我们需要运行此回调,并且 callback 自身也有两个参数,分别是 resolverrejecter ,他们也是回调函数的形式;
  • 定义了几个变量保存当前的一些结果与状态、事件队列,见注释;
  • 执行函数 callback 时,如果是 resolve 状态,将结果保存在 this.__succ_res 中,状态标记为成功;如果是 reject 状态,操作类似;
  • 同时定义了最常用的 then 方法,是一个原型方法;
  • 执行 then 方法时,判断对象的状态是成功还是失败,分别执行对应的回调,把结果传入回调处理。
//几个状态常量
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function MyPromise(callback) {
    this.status = PENDING; //储存状态
    this.__succ__res = null; //储存resolve结果
    this.__err__res = null; //储存reject结果
    var _this = this;//必须处理this的指向
    function resolver(res) {
        _this.status = FULFILLED;
        _this.__succ__res = res;
    };
    function rejecter(rej) {
        _this.status = REJECTED;
        _this.__err__res = rej;
    };
    callback(resolver, rejecter);
};
MyPromise.prototype.then = function(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
        onFulfilled(this.__succ__res);
    } else if (this.status === REJECTED) {
        onRejected(this.__err__res);
    };
};
复制代码

到这里,MyPromise 可以简单实现一些同步代码,比如:

new MyPromise((resolve, reject) => {
    resolve(1);
}).then(res => {
    console.log(res);
});
//结果 1
复制代码

第二步,加入异步处理

执行异步代码时,then 方法会先于异步结果执行,上面的处理还无法获取到结果。

  • 首先,既然是异步,then 方法在 pending 状态时就执行了,所以添加一个 else
  • 执行 else 时,我们还没有结果,只能把需要执行的回调,放到一个队列里,等需要时执行它,所以定义了一个新变量 this.__queue 保存事件队列;
  • 当异步代码执行完毕,这时候把 this.__queue 队列里的回调统统执行一遍,如果是 resolve 状态,则执行对应的 resolve 代码。
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function MyPromise(callback) {
    this.status = PENDING; //储存状态
    this.__succ__res = null; //储存resolve结果
    this.__err__res = null; //储存reject结果
    this.__queue = []; //++     事件队列

    var _this = this;
    function resolver(res) {
        _this.status = FULFILLED;
        _this.__succ__res = res;
        _this.__queue.forEach(item => {//++     队列中事件的执行
            item.resolve(res);
        });
    };
    function rejecter(rej) {
        _this.status = REJECTED;
        _this.__err__res = rej;
        _this.__queue.forEach(item => {//++     队列中事件的执行
            item.reject(rej);
        });
    };
    callback(resolver, rejecter);
};

MyPromise.prototype.then = function(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
        onFulfilled(this.__succ__res);
    } else if (this.status === REJECTED) {
        onRejected(this.__err__res);
    } else {//++        pending状态,添加队列事件
        this.__queue.push({resolve: onFulfilled, reject: onRejected});
    };
};
复制代码

到这一步,MyPromise 已经可以实现一些简单的异步代码了。测试用例 index.html 中,这两个例子已经可以实现了。

  • 1 异步测试--resolve
  • 2 异步测试--reject

第三步,加入链式调用

实际上,原生的 Promise 对象的then方法,返回的也是一个 Promise 对象,一个新的 Promise 对象,这样才可以支持链式调用,一直then下去。。。 而且,then方法可以接收到上一个then方法处理return的结果。根据Promise的特性分析,这个返回结果有3种可能:

  1. MyPromise对象;
  2. 具有then方法的对象;
  3. 其他值。 根据这三种情况分别处理。
  • 第一个处理的是,then方法返回一个MyPromise对象,它的回调函数接收resFnrejFn 两个回调函数;
  • 把成功状态的处理代码封装为handleFulfilled函数,接受成功的结果作为参数;
  • handleFulfilled函数中,根据onFulfilled返回值的不同,做不同的处理:
    • 首先,先获取onFulfilled的返回值(如果有),保存为returnVal
    • 然后,判断returnVal是否有then方法,即包括上面讨论的1、2中情况(它是MyPromise对象,或者具有then方法的其他对象),对我们来说都是一样的;
    • 之后,如果有then方法,马上调用其then方法,分别把成功、失败的结果丢给新MyPromise对象的回调函数;没有则结果传给resFn回调函数。

reject状态的链式调用的处理思路是类似的,在定义的handleRejected函数中,检查onRejected返回的结果是否含then方法,分开处理。值得一提的是,如果返回的是普通值,应该调用的是resFn,而不是rejFn,因为这个返回值属于新MyPromise对象,它的状态不因当前MyPromise对象的状态而确定。即是,返回了普通值,未表明reject状态,我们默认为resolve状态。

MyPromise.prototype.then = function(onFulfilled, onRejected) {
    var _this = this;
    return new MyPromise(function(resFn, rejFn) {
        if (_this.status === FULFILLED) {
            handleFulfilled(_this.__succ__res);     // -+
        } else if (_this.status === REJECTED) {
            handleRejected(_this.__err__res);       // -+
        } else {//pending状态
            _this.__queue.push({resolve: handleFulfilled, reject: handleRejected}); // -+
        };

        function handleFulfilled(value) {   // ++   FULFILLED 状态回调
            // 取决于onFulfilled的返回值
            var returnVal = onFulfilled instanceof Function && onFulfilled(value) || value;
            if (returnVal['then'] instanceof Function) {
                returnVal.then(function(res) {
                    resFn(res);
                },function(rej) {
                    rejFn(rej);
                });
            } else {
                resFn(returnVal);
            };
        };
        function handleRejected(reason) {   // ++   REJECTED 状态回调
            if (onRejected instanceof Function) {
                var returnVal = onRejected(reason);
                if (typeof returnVal !== 'undefined' && returnVal['then'] instanceof Function) {
                    returnVal.then(function(res) {
                        resFn(res);
                    },function(rej) {
                        rejFn(rej);
                    });
                } else {
                    resFn(returnVal);
                };
            } else {
                rejFn(reason)
            }
        }

    })
};
复制代码

现在,MyPromise对象已经很好地支持链式调用了,测试例子:

  • 4 链式调用--resolve
  • 5 链式调用--reject
  • 28 then回调返回Promise对象(reject)
  • 29 then方法reject回调返回Promise对象

第四步,MyPromise.resolve()和MyPromise.reject()方法实现

因为其它方法对MyPromise.resolve()方法有依赖,所以先实现这个方法。
先要完全弄懂MyPromise.resolve()方法的特性,研究了阮一峰老师的ECMAScript 6 入门对于MyPromise.resolve()方法的描述部分。 由此得知,这个方法功能很简单,就是把参数转换成一个MyPromise对象,关键点在于参数的形式,分别有:

  • 参数是一个 MyPromise 实例;
  • 参数是一个thenable对象;
  • 参数不是具有then方法的对象,或根本就不是对象;
  • 不带有任何参数。

处理的思路是:

  • 首先考虑极端情况,参数是undefined或者null的情况,直接处理原值传递;
  • 其次,参数是MyPromise实例时,无需处理;
  • 然后,参数是其它thenable对象的话,调用其then方法,把相应的值传递给新MyPromise对象的回调;
  • 最后,就是普通值的处理。

MyPromise.reject()方法相对简单很多。与MyPromise.resolve()方法不同,MyPromise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。

MyPromise.resolve = function(arg) {
    if (typeof arg === 'undefined' || arg === null) {   //undefined 或者 null
        return new MyPromise(function(resolve) {
            resolve(arg);
        });
    } else if (arg instanceof MyPromise) {      // 参数是MyPromise实例
        return arg;
    } else if (arg['then'] instanceof Function) {   // 参数是thenable对象
        return new MyPromise(function(resolve, reject) {
            arg.then(function (res) {
                resolve(res);
            }, function (rej) {
                reject(rej);
            });
        });
    } else {    // 其他值
        return new MyPromise(function (resolve) {
            resolve(arg);
        });
    };
};
MyPromise.reject = function(arg) {
    return  new MyPromise(function(resolve, reject) {
        reject(arg);
    });
};
复制代码

测试用例有8个:18-25,感兴趣可以玩一下。

第五步,MyPromise.all()和MyPromise.race()方法实现

MyPromise.all()方法接收一堆MyPromise对象,当他们都成功时,才执行回调。依赖MyPromise.resolve()方法把不是MyPromise的参数转为MyPromise对象。
每个对象执行then方法,把结果存到一个数组中,当他们都执行完毕后,即i === arr.length,才调用resolve()回调,把结果传进去。
MyPromise.race()方法也类似,区别在于,这里做的是一个done标识,如果其中之一改变了状态,不再接受其他改变。

MyPromise.all = function(arr) {
    if (!Array.isArray(arr)) {
        throw new TypeError('参数应该是一个数组!');
    };
    return new MyPromise(function(resolve, reject) {
        var i = 0, result = [];
        next();
        function next() {
            // 对于不是MyPromise实例的进行转换
            MyPromise.resolve(arr[i]).then(function (res) {
                result.push(res);
                i++;
                if (i === arr.length) {
                    resolve(result);
                } else {
                    next();
                };
            }, reject);
        }
    })
};
MyPromise.race =  function(arr) {
    if (!Array.isArray(arr)) {
        throw new TypeError('参数应该是一个数组!');
    };
    return new MyPromise(function(resolve, reject) {
        let done = false;
        arr.forEach(function(item) {
            MyPromise.resolve(item).then(function (res) {
                if (!done) {
                    resolve(res);
                    done = true;
                };
            }, function(rej) {
                if (!done) {
                    reject(rej);
                    done = true;
                };
            });
        })
    });
};
复制代码

测试用例:

  • 6 all方法
  • 26 race方法测试

第六步,Promise.prototype.catch()和Promise.prototype.finally()方法实现

他们俩本质上是then方法的一种延伸,特殊情况的处理。

MyPromise.prototype.catch = function(errHandler) {
    return this.then(undefined, errHandler);
};
MyPromise.prototype.finally = function(finalHandler) {
    return this.then(finalHandler, finalHandler);
};
复制代码

测试用例:

  • 7 catch测试
  • 16 finally测试——异步代码错误
  • 17 finally测试——同步代码错误

第七步,代码错误的捕获

目前而言,我们的catch还不具备捕获代码报错的能力。思考,错误的代码来自于哪里?肯定是使用者的代码,2个来源分别有:

  • MyPromise对象构造函数回调
  • then方法的2个回调 捕获代码运行错误的方法是原生的try...catch...,所以我用它来包裹这些回调运行,捕获到的错误进行相应处理。
function MyPromise(callback) {
    this.status = PENDING; //储存状态
    this.__succ__res = null; //储存resolve结果
    this.__err__res = null; //储存reject结果
    this.__queue = []; //事件队列

    var _this = this;
    function resolver(res) {
        _this.status = FULFILLED;
        _this.__succ__res = res;
        _this.__queue.forEach(item => {
            item.resolve(res);
        });
    };
    function rejecter(rej) {
        _this.status = REJECTED;
        _this.__err__res = rej;
        _this.__queue.forEach(item => {
            item.reject(rej);
        });
    };
    try {   // -+   在try……catch……中运行回调函数
        callback(resolver, rejecter);
    } catch (err) {
        this.__err__res = err;
        this.status = REJECTED;
        this.__queue.forEach(function(item) {
            item.reject(err);
        });
    };
};

MyPromise.prototype.then = function(onFulfilled, onRejected) {
    var _this = this;
    return new MyPromise(function(resFn, rejFn) {
        if (_this.status === FULFILLED) {
            handleFulfilled(_this.__succ__res);
        } else if (_this.status === REJECTED) {
            handleRejected(_this.__err__res);
        } else {//pending状态
            _this.__queue.push({resolve: handleFulfilled, reject: handleRejected});
        };

        function handleFulfilled(value) {
            var returnVal = value;
            // 获取 onFulfilled 函数的返回结果
            if (onFulfilled instanceof Function) {
                try {       // -+   在try……catch……中运行onFulfilled回调函数
                    returnVal = onFulfilled(value);
                } catch (err) { // 代码错误处理
                    rejFn(err);
                    return;
                };
            };

            if (returnVal && returnVal['then'] instanceof Function) {
                returnVal.then(function(res) {
                    resFn(res);
                },function(rej) {
                    rejFn(rej);
                });
            } else {
                resFn(returnVal);
            };
        };
        function handleRejected(reason) {
            if (onRejected instanceof Function) {
                var returnVal
                try {// -+   在try……catch……中运行onRejected回调函数
                    returnVal = onRejected(reason);
                } catch (err) {
                    rejFn(err);
                    return;
                };
                if (typeof returnVal !== 'undefined' && returnVal['then'] instanceof Function) {
                    returnVal.then(function(res) {
                        resFn(res);
                    },function(rej) {
                        rejFn(rej);
                    });
                } else {
                    resFn(returnVal);
                };
            } else {
                rejFn(reason)
            }
        }

    })
};
复制代码

测试用例:

  • 11 catch测试——代码错误捕获
  • 12 catch测试——代码错误捕获(异步)
  • 13 catch测试——then回调代码错误捕获
  • 14 catch测试——代码错误catch捕获

其中第12个异步代码错误测试,结果显示是直接报错,没有捕获错误,原生的Promise也是这样的,我有点不能理解为啥不捕获处理它。

第八步,处理MyPromise状态确定不允许再次改变

这是Promise的一个关键特性,处理起来不难,在执行回调时加入状态判断,如果已经是成功或者失败状态,则不运行回调代码。

function MyPromise(callback) {
    //略……

    var _this = this;
    function resolver(res) {
        if (_this.status === PENDING) {
            _this.status = FULFILLED;
            _this.__succ__res = res;
            _this.__queue.forEach(item => {
                item.resolve(res);
            });
        };
    };
    function rejecter(rej) {
        if (_this.status === PENDING) {
            _this.status = REJECTED;
            _this.__err__res = rej;
            _this.__queue.forEach(item => {
                item.reject(rej);
            });
        };
    };
    
    //略……
};
复制代码

测试用例:

  • 27 Promise状态多次改变

第九步,onFulfilled 和 onRejected 方法异步执行

到这里为止,如果执行下面一段代码,

function test30() {
  function fn30(resolve, reject) {
      console.log('running fn30');
      resolve('resolve @fn30')
  };
  console.log('start');
  let p = new MyPromise(fn30);
  p.then(res => {
      console.log(res);
  }).catch(err => {
      console.log('err=', err);
  });
  console.log('end');
};
复制代码

输出结果是:

//MyPromise结果
// start
// running fn30
// resolve @fn30
// end

//原生Promise结果:
// start
// running fn30
// end
// resolve @fn30
复制代码

两个结果不一样,因为onFulfilled 和 onRejected 方法不是异步执行的,需要做以下处理,将它们的代码放到本轮任务队列的末尾执行。

function MyPromise(callback) {
    //略……

    var _this = this;
    function resolver(res) {
        setTimeout(() => {      //++ 利用setTimeout调整任务执行队列
            if (_this.status === PENDING) {
                _this.status = FULFILLED;
                _this.__succ__res = res;
                _this.__queue.forEach(item => {
                    item.resolve(res);
                });
            };            
        }, 0);
    };
    function rejecter(rej) {
        setTimeout(() => {      //++
            if (_this.status === PENDING) {
                _this.status = REJECTED;
                _this.__err__res = rej;
                _this.__queue.forEach(item => {
                    item.reject(rej);
                });
            };            
        }, 0);
    };
    
    //略……
};
复制代码

测试用例:

  • 30 then方法的异步执行

以上,是我所有的代码书写思路、过程。完整代码与测试代码到github下载


参考文章

  • ECMAScript 6 入门 - Promise 对象
  • es6 promise源码实现
  • 手把手教你实现一个完整的 Promise
  • Promises/A+规范
  • 阮一峰的网络日志:JavaScript 运行机制详解:再谈Event Loop

转载于:https://juejin.im/post/5c4576cf6fb9a04a06052f3f

你可能感兴趣的:(原生es5封装的Promise对象)