原生es5封装的Promise对象

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

内容简介:前一阵看了一些关于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对象》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

Implementing Responsive Design

Implementing Responsive Design

Tim Kadlec / New Riders / 2012-7-31 / GBP 27.99

New devices and platforms emerge daily. Browsers iterate at a remarkable pace. Faced with this volatile landscape we can either struggle for control or we can embrace the inherent flexibility of the w......一起来看看 《Implementing Responsive Design》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

随机密码生成器
随机密码生成器

多种字符组合密码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具