前言
前一阵看了一些关于JS异步操作的文章,发现Promise
真是个好东西,配合Generator
或者async/await
使用更有奇效。完美解决异步代码书写的回调问题,有助于书写更优雅的异步代码。花了几天时间研究了Promise
的工作机制,手痒痒用es6语法封装了一个Promise
对象,基本实现了原生Promise
的功能,现在,用es5语法再写一遍。
更新说明
- 更新时间:2019/1/23
经@logbn520
兄弟的提醒,我把then方法的执行做成同步的了,是不符合规范的。
中,【Then 方法】小节【调用时机】部分写道:“onFulfilled 和 onRejected 只有在执行环境堆栈仅包含平台代码时才可被调用”,这里特别要看一下注释。
因此我要把onFulfilled
和 onRejected
的代码放在“ then
方法被调用的那一轮事件循环之后的新执行栈中执行”,通过setTimeout
方法将任务放到本轮任务队列的末尾。代码已添加到最后一部分-第九步。
关于任务队列的运行机制,感兴趣可看一下阮一峰老师的
实现功能:
- 已实现
Promise
基本功能,与原生一样,异步、同步操作均ok,具体包括:MyPromise.prototype.then()
MyPromise.prototype.catch()
与原生Promise
略有出入MyPromise.prototype.finally()
MyPromise.all()
MyPromise.race()
MyPromise.resolve()
MyPromise.reject()
rejected
状态的冒泡处理也已解决,当前Promise的reject如果没有捕获,会一直冒泡到最后,直到catchMyPromise
状态一旦改变,将不能再改变它的状态
不足之处:
- 代码的错误被catch捕获时,提示的信息(捕获的错误对象)比原生Promise要多
测试: index.html
- 这个页面中包含了30个测试例子,分别测试了各项功能、各个方法,还有一些特殊情况测试;或许还有有遗漏的,感兴趣自己可以玩一下;
- 更加友好的可视化的操作,方便测试,每次运行一个例子,右边面板可看到结果;
- 自定义了
console.mylog()
方法用来输出结果,第一个参数是当前使用的Promise
对象,用以区分输出,查看代码时可忽略,后面的参数都是输出结果,与系统console.log()
相似; - 建议同时打开
index.js
边看代码边玩; - 同一套代码,上面的
MyPromise
的运行结果,下面是原生Promise
运行的结果;
收获
- 再写一遍又有新收获,写的更顺了,理解更深刻了;
then/catch
方法是最难的,要不停地修修补补;reject
状态的冒泡是个难题,但在下面的代码中我没有专门提及,我也没有办法具体说清楚他,我是在整个过程中不停地调才最终调出来正确的冒泡结果。
代码
下面贴代码,包括整个思考过程,会有点长
为了说明书写的逻辑,我使用以下几个注释标识,整坨变动的代码只标识这一坨的开头处。//++
——添加的代码//-+
——修改的代码
第一步,基础功能实现
名字随便取,我的叫MyPromise,没有取代原生的Promise。
- 构造函数传入回调函数
callback
。当新建MyPromise
对象时,我们需要运行此回调,并且callback
自身也有两个参数,分别是resolver
和rejecter
,他们也是回调函数的形式; - 定义了几个变量保存当前的一些结果与状态、事件队列,见注释;
- 执行函数
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
对象,它的回调函数接收resFn
和rejFn
两个回调函数; - 把成功状态的处理代码封装为
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()
方法的特性,研究了阮一峰老师的对于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
也是这样的,我有点不能理解为啥不捕获处理它。
第八步,处理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状态多次改变
第九步,onFulfilled 和 onRejected 方法异步执行
到这里为止,如果执行下面一段代码,
function test30() { function fn30(resolve, reject) { console.log('running fn30'); resolve('resolve @fn30') }; console.log('start'); let p = new MyPromise(fn30); p.then(res => { console.log(res); }).catch(err => { console.log('err=', err); }); console.log('end');};复制代码
输出结果是:
//MyPromise结果// start// running fn30// resolve @fn30// end//原生Promise结果:// start// running fn30// end// resolve @fn30复制代码
两个结果不一样,因为onFulfilled 和 onRejected 方法不是异步执行的,需要做以下处理,将它们的代码放到本轮任务队列的末尾执行。
function MyPromise(callback) { //略…… var _this = this; function resolver(res) { setTimeout(() => { //++ 利用setTimeout调整任务执行队列 if (_this.status === PENDING) { _this.status = FULFILLED; _this.__succ__res = res; _this.__queue.forEach(item => { item.resolve(res); }); }; }, 0); }; function rejecter(rej) { setTimeout(() => { //++ if (_this.status === PENDING) { _this.status = REJECTED; _this.__err__res = rej; _this.__queue.forEach(item => { item.reject(rej); }); }; }, 0); }; //略……};复制代码
测试用例:
30 then方法的异步执行
以上,是我所有的代码书写思路、过程。完整代码与测试代码到下载