原生es6封装一个Promise对象
栏目: JavaScript · 发布时间: 6年前
内容简介:下面贴代码,包括整个思考过程,会有点长为了说明书写的逻辑,我使用以下几个注释标识,整坨变动的代码只标识这一坨的开头处。
- 已实现
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跨域请求
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Black Box Society
Frank Pasquale / Harvard University Press / 2015-1-5 / USD 35.00
Every day, corporations are connecting the dots about our personal behavior—silently scrutinizing clues left behind by our work habits and Internet use. The data compiled and portraits created are inc......一起来看看 《The Black Box Society》 这本书的介绍吧!