原生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
自身也有两个参数,分别是resolve
和reject
,他们也是回调函数的形式; - 定义了几个变量保存当前的一些结果与状态、事件队列,见注释;
- 执行函数
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
对象,它的回调函数接收resFn
和rejFn
两个回调函数; - 把成功状态的处理代码封装为
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...
,所以我用它来包裹这些回调运行,捕获到的错误进行相应处理。
为确保代码清晰,提取了 resolver
、 rejecter
两个函数,因为是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
也是这样的,我有点不能理解为啥不捕获处理它。
第八步,处理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 下载
参考文章
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- ios - 原生骨架库,网络过渡动画封装
- 原生es5封装的Promise对象
- 原生JS简单封装JSONP跨域获取数据
- ios - 原生骨架屏,网络加载过渡动画的封装
- 基于原生fetch封装一个带有拦截器功能的fetch,类似axios的拦截器
- 封装一个原生js的ajax请求,支持IE9CORS跨域请求
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。