原生es5封装的Promise对象

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

内容简介:前一阵看了一些关于JS异步操作的文章,发现下面贴代码,包括整个思考过程,会有点长为了说明书写的逻辑,我使用以下几个注释标识,整坨变动的代码只标识这一坨的开头处。

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

实现功能:

  • 已实现 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种可能:

MyPromise
then
  • 第一个处理的是, 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 也是这样的,我有点不能理解为啥不捕获处理它。

原生es5封装的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状态多次改变

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

参考文章


以上所述就是小编给大家介绍的《原生es5封装的Promise对象》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

匠心体验

匠心体验

[法] 艾米丽·布歇 / 吴 博 / 人民邮电出版社 / 2016-11 / 69.00元

本书针对在智能手机和平板电脑的网站及应用程序设计,详细剖析了移动终端服务的用户体验设计要点,阐述了营造舒适的感官体验、甄选内容及功能、提高用户效率、优化等待时间、合理实施教学、情感设计等方面的设计诀窍,并通过大量实例,呈现当今移动终端服务设计中的亮点与雷区。一起来看看 《匠心体验》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

SHA 加密
SHA 加密

SHA 加密工具

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

HEX CMYK 互转工具