原生es6封装一个Promise对象

栏目: JavaScript · 发布时间: 5年前

内容简介:下面贴代码,包括整个思考过程,会有点长为了说明书写的逻辑,我使用以下几个注释标识,整坨变动的代码只标识这一坨的开头处。
  • 已实现 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要多
  • 代码是es6写的,会考虑再用es5写,以便于应用到es5项目中;es5写的话,不用箭头函数,要考虑this的问题

测试: index.html

  • 这个页面中包含了27个测试例子,分别测试了各项功能、各个方法,还有一些特殊情况测试;或许还有有遗漏的,感兴趣自己可以玩一下;
  • 可视化的操作,方便测试,每次运行一个例子,打开调试台即可看到结果;建议同时打开 index.js 边看代码边玩;
  • 同一套代码,上面的 MyPromise 的运行结果,下面是原生 Promise 运行的结果;

收获

Promise
then/catch
reject

代码

下面贴代码,包括整个思考过程,会有点长

为了说明书写的逻辑,我使用以下几个注释标识,整坨变动的代码只标识这一坨的开头处。

//++ ——添加的代码

//-+ ——修改的代码

第一步,定义MyPromise类

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

  • 构造函数传入回调函数 callback 。当新建 MyPromise 对象时,我们需要运行此回调,并且 callback 自身也有两个参数,分别是 resolvereject ,他们也是回调函数的形式;
  • 定义了几个变量保存当前的一些结果与状态、事件队列,见注释;
  • 执行函数 callback 时,如果是 resolve 状态,将结果保存在 this.__succ_res 中,状态标记为成功;如果是 reject 状态,操作类似;
  • 同时定义了最常用的 then 方法,是一个原型方法;
  • 执行 then 方法时,判断对象的状态是成功还是失败,分别执行对应的回调,把结果传入回调处理;
  • 这里接收 ...arg 和传入参数 ...this.__succ_res 都使用了扩展运算符,为了应对多个参数的情况,原封不动地传给 then 方法回调。

callback 回调这里使用箭头函数, this 的指向就是本当前 MyPromise 对象,所以无需处理 this 问题。

class MyPromise {
    constructor(callback) {
        this.__succ_res = null;     //保存成功的返回结果
        this.__err_res = null;      //保存失败的返回结果
        this.status = 'pending';    //标记处理的状态
        //箭头函数绑定了this,如果使用es5写法,需要定义一个替代的this
        callback((...arg) => {
            this.__succ_res = arg;
            this.status = 'success';
        }, (...arg) => {
            this.__err_res = arg;
            this.status = 'error';
        });
    }
    then(onFulfilled, onRejected) {
        if (this.status === 'success') {
            onFulfilled(...this.__succ_res);
        } else if (this.status === 'error') {
            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 代码。
class MyPromise {
    constructor(fn) {
        this.__succ_res = null;     //保存成功的返回结果
        this.__err_res = null;      //保存失败的返回结果
        this.status = 'pending';    //标记处理的状态
        this.__queue = [];          //事件队列		//++
        //箭头函数绑定了this,如果使用es5写法,需要定义一个替代的this
        fn((...arg) => {
            this.__succ_res = arg;
            this.status = 'success';
            this.__queue.forEach(json => {			//++
               json.resolve(...arg);
            });
        }, (...arg) => {
            this.__err_res = arg;
            this.status = 'error';
            this.__queue.forEach(json => {			//++
                json.reject(...arg);
            });
        });
    }
    then(onFulfilled, onRejected) {
        if (this.status === 'success') {
            onFulfilled(...this.__succ_res);
        } else if (this.status === 'error') {
            onRejected(...this.__err_res);
        } else {										//++
            this.__queue.push({resolve: onFulfilled, reject: onRejected});
        };
    }
};
复制代码

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

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

第三步,加入链式调用

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

MyPromise
then
  • 第一个处理的是, then 方法返回一个 MyPromise 对象,它的回调函数接收 resFnrejFn 两个回调函数;
  • 把成功状态的处理代码封装为 handle 函数,接受成功的结果作为参数;
  • handle 函数中,根据 onFulfilled 返回值的不同,做不同的处理:
    • 首先,先获取 onFulfilled 的返回值(如果有),保存为 returnVal
    • 然后,判断 returnVal 是否有then方法,即包括上面讨论的1、2中情况(它是 MyPromise 对象,或者具有 then 方法的其他对象),对我们来说都是一样的;
    • 之后,如果有 then 方法,马上调用其 then 方法,分别把成功、失败的结果丢给新 MyPromise 对象的回调函数;没有则结果传给 resFn 回调函数。
class MyPromise {
    constructor(fn) {
        this.__succ_res = null;     //保存成功的返回结果
        this.__err_res = null;      //保存失败的返回结果
        this.status = 'pending';    //标记处理的状态
        this.__queue = [];          //事件队列
        //箭头函数绑定了this,如果使用es5写法,需要定义一个替代的this
        fn((...arg) => {
            this.__succ_res = arg;
            this.status = 'success';
            this.__queue.forEach(json => {
                json.resolve(...arg);
            });
        }, (...arg) => {
            this.__err_res = arg;
            this.status = 'error';
            this.__queue.forEach(json => {
                json.reject(...arg);
            });
        });
    }
    then(onFulfilled, onRejected) {
        return new MyPromise((resFn, rejFn) => {							//++
            if (this.status === 'success') {
               handle(...this.__succ_res);								//-+
            } else if (this.status === 'error') {
                onRejected(...this.__err_res);
            } else {
               this.__queue.push({resolve: handle, reject: onRejected});        //-+
            };
            function handle(value) {									//++
                //then方法的onFulfilled有return时,使用return的值,没有则使用保存的值
                let returnVal = onFulfilled instanceof Function && onFulfilled(value) || value;
                //如果onFulfilled返回的是新MyPromise对象或具有then方法对象,则调用它的then方法
                if (returnVal && returnVal['then'] instanceof Function) {
                    returnVal.then(res => {
                        resFn(res);
                    }, err => {
                        rejFn(err);
                    });
                } else {//其他值
                    resFn(returnVal);
                };
            };
        })
    }
};
复制代码

到这里, MyPromise 对象已经支持链式调用了,测试例子: 4 链式调用--resolve 。但是,很明显,我们还没完成 reject 状态的链式调用。

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

代码过长,只展示改动部分。

then(onFulfilled, onRejected) {
    return new MyPromise((resFn, rejFn) => {
        if (this.status === 'success') {
            handle(...this.__succ_res);
        } else if (this.status === 'error') {
           errBack(...this.__err_res);									//-+
        } else {
           this.__queue.push({resolve: handle, reject: errBack});			//-+
        };
        function handle(value) {
            //then方法的onFulfilled有return时,使用return的值,没有则使用保存的值
            let returnVal = onFulfilled instanceof Function && onFulfilled(value) || value;
            //如果onFulfilled返回的是新MyPromise对象或具有then方法对象,则调用它的then方法
            if (returnVal && returnVal['then'] instanceof Function) {
                returnVal.then(res => {
                    resFn(res);
                }, err => {
                    rejFn(err);
                });
            } else {//其他值
                resFn(returnVal);
            };
        };
        function errBack(reason) {										//++
	        if (onRejected instanceof Function) {
	        	//如果有onRejected回调,执行一遍
	            let returnVal = onRejected(reason);
	            //执行onRejected回调有返回,判断是否thenable对象
	            if (typeof returnVal !== 'undefined' && returnVal['then'] instanceof Function) {
	                returnVal.then(res => {
	                    resFn(res);
	                }, err => {
	                    rejFn(err);
	                });
	            } else {
	                //无返回或者不是thenable的,直接丢给新对象resFn回调
	                resFn(returnVal);				//resFn,而不是rejFn
	            };
	        } else {//传给下一个reject回调
	            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 = (arg) => {
    if (typeof arg === 'undefined' || arg == null) {//无参数/null
        return new MyPromise((resolve) => {
            resolve(arg);
        });
    } else if (arg instanceof MyPromise) {
        return arg;
    } else if (arg['then'] instanceof Function) {
        return new MyPromise((resolve, reject) => {
            arg.then((res) => {
                resolve(res);
            }, err => {
                reject(err);
            });
        });
    } else {
        return new MyPromise(resolve => {
            resolve(arg);
        });
    }
};
MyPromise.reject = (arg) => {
    return new MyPromise((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 = (arr) => {
    if (!Array.isArray(arr)) {
        throw new TypeError('参数应该是一个数组!');
    };
    return new MyPromise(function(resolve, reject) {
        let i = 0, result = [];
        next();
        function next() {
            //如果不是MyPromise对象,需要转换
            MyPromise.resolve(arr[i]).then(res => {
                result.push(res);
                i++;
                if (i === arr.length) {
                    resolve(result);
                } else {
                    next();
                };
            }, reject);
        };
    })
};
MyPromise.race = arr => {
    if (!Array.isArray(arr)) {
        throw new TypeError('参数应该是一个数组!');
    };
    return new MyPromise((resolve, reject) => {
        let done = false;
        arr.forEach(item => {
            //如果不是MyPromise对象,需要转换
            MyPromise.resolve(item).then(res => {
                if (!done) {
                    resolve(res);
                    done = true;
                };
            }, err => {
                if (!done) {
                    reject(err);
                    done = true;
                };
            });
        })
    })

}
复制代码

测试用例:

6 all方法
26 race方法测试

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

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

catch代码中注释部分是我原来的解决思路:运行catch时,如果已经是错误状态,则直接运行回调;如果是其它状态,则把回调函数推入事件队列,待最后接收到前面reject状态时执行;因为catch直接收reject状态,所以队列中resolve是个空函数,防止报错。

后来看了参考文章3才了解到还有更好的写法,因此替换了。

class MyPromise {
	constructor(fn) {
		//...略
	}
    then(onFulfilled, onRejected) {
    	//...略
	}
    catch(errHandler) {
        // if (this.status === 'error') {
        //     errHandler(...this.__err_res);
        // } else {
        //     this.__queue.push({resolve: () => {}, reject: errHandler});
        //     //处理最后一个Promise的时候,队列resolve推入一个空函数,不造成影响,不会报错----如果没有,则会报错
        // };
        return this.then(undefined, errHandler);
    }
    finally(finalHandler) {
        return this.then(finalHandler, finalHandler);
    }
};
复制代码

测试用例:

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

第七步,代码错误的捕获

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

  • MyPromise 对象构造函数回调
  • then 方法的2个回调 捕获代码运行错误的方法是原生的 try...catch... ,所以我用它来包裹这些回调运行,捕获到的错误进行相应处理。

为确保代码清晰,提取了 resolverrejecter 两个函数,因为是es5写法,需要手动处理 this 指向问题

class MyPromise {
    constructor(fn) {
        this.__succ_res = null;     //保存成功的返回结果
        this.__err_res = null;      //保存失败的返回结果
        this.status = 'pending';    //标记处理的状态
        this.__queue = [];          //事件队列
        //定义function需要手动处理this指向问题
        let _this = this;									//++
        function resolver(...arg) {							//++
            _this.__succ_res = arg;
            _this.status = 'success';
            _this.__queue.forEach(json => {
                json.resolve(...arg);
            });
        };
        function rejecter(...arg) {							//++
	        _this.__err_res = arg;
	        _this.status = 'error';
	        _this.__queue.forEach(json => {
	            json.reject(...arg);
	        });
        };
        try {												//++
       		fn(resolver, rejecter);							//-+
        } catch(err) {										//++
            this.__err_res = [err];
            this.status = 'error';
            this.__queue.forEach(json => {
                json.reject(...err);
            });
        };
    }
    then(onFulfilled, onRejected) {
        //箭头函数绑定了this,如果使用es5写法,需要定义一个替代的this
        return new MyPromise((resFn, rejFn) => {
            function handle(value) {
                //then方法的onFulfilled有return时,使用return的值,没有则使用回调函数resolve的值
                let returnVal = value;						//-+
                if (onFulfilled instanceof Function) {		//-+
                    try {									//++
                        returnVal = onFulfilled(value);
                    } catch(err) {							//++
                        //代码错误处理
                        rejFn(err);
                        return;
                    }
                };
                if (returnVal && returnVal['then'] instanceof Function) {
                	//如果onFulfilled返回的是新Promise对象,则调用它的then方法
                    returnVal.then(res => {
                        resFn(res);
                    }, err => {
                        rejFn(err);
                    });
                } else {
                    resFn(returnVal);
                };
            };
            function errBack(reason) {
                //如果有onRejected回调,执行一遍
                if (onRejected instanceof Function) {
                    try {													//++
                        let returnVal = onRejected(reason);
                        //执行onRejected回调有返回,判断是否thenable对象
                        if (typeof returnVal !== 'undefined' && returnVal['then'] instanceof Function) {
                            returnVal.then(res => {
                                resFn(res);
                            }, err => {
                                rejFn(err);
                            });
                        } else {
                            //不是thenable的,直接丢给新对象resFn回调
                            resFn(returnVal);
                        };
                    } catch(err) {											//++
                        //代码错误处理
                        rejFn(err);
                        return;
                    }
                } else {//传给下一个reject回调
                    rejFn(reason);
                };
            };
            if (this.status === 'success') {
                handle(...this.__succ_res);
            } else if (this.status === 'error') {
                errBack(...this.__err_res);
            } else {
                this.__queue.push({resolve: handle, reject: errBack});
            };
        })
    }
};
复制代码

测试用例:

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

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

原生es6封装一个Promise对象

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

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

class MyPromise {
    constructor(fn) {
        this.__succ_res = null;     //保存成功的返回结果
        this.__err_res = null;      //保存失败的返回结果
        this.status = 'pending';    //标记处理的状态
        this.__queue = [];          //事件队列
        //箭头函数绑定了this,如果使用es5写法,需要定义一个替代的this
        let _this = this;
        function resolver(...arg) {
            if (_this.status === 'pending') {				//++
                //如果状态已经改变,不再执行本代码
                _this.__succ_res = arg;
                _this.status = 'success';
                _this.__queue.forEach(json => {
                    json.resolve(...arg);
                });
            };
        };
        function rejecter(...arg) {
            if (_this.status === 'pending') {				//++
                //如果状态已经改变,不再执行本代码
                _this.__err_res = arg;
                _this.status = 'error';
                _this.__queue.forEach(json => {
                    json.reject(...arg);
                });
            };
        };
        try {
            fn(resolver, rejecter);
        } catch(err) {
            this.__err_res = [err];
            this.status = 'error';
            this.__queue.forEach(json => {
                json.reject(...err);
            });
        };
    }
    //...略
};
复制代码

测试用例:

  • 27 Promise状态多次改变

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

参考文章


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web导航设计

Web导航设计

James Kalbach / 李曦琳 / 电子工业出版社 / 2009 年3月 / 69.80元

业务目标的实现,依赖于用户能够找到并使用您提供的服务。本书为您讲述创建有效导航系统的基本设计原则、开发技巧和实用建议,并附有大量的真实案例。本书研究深入,援引广泛,是极佳的参考资料和教学指南,适用于初级和中级网页设计师、产品经理和其他非设计职位,以及寻求全新视角的Web开发老手。一起来看看 《Web导航设计》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具