Promise简介
Promise 采用面向对象的方式封装了回调函数,可以将回调金字塔改为平行的链式写法,优雅的解决了回调地狱,ES7带来了异步的终级解决方案async/await,可以用写同步代码的方式编写异步代码,而Promise正是async/await的基石。
Promise 是一种设计模式,也是规范,历史上曾经出现过Promise A/Promise A+/Promise B/Promise D四种规范,最终ES6选择了Promise A+的方案,真理来之不易。
Promise表面上看起来比较简单,你看,生成的Promise对象很纯净,只有then,catch,finally几个方法,还有两个隐藏的属性:PromiseStatus和PromiseValue分别表示状态和返回值
说到Promise状态,众所周知,只有pending,fulfilled,rejected 3种状态,而且不可逆,不成功便成仁,从创建实例时的pending,调用成功resolve就变成fulfilled,失败则变为rejected,整个模型非常简单
但是深入了解,Promise还有许多的潜规则,要深入理解一项技术最好的办法是造一个轮子。 Promise能够一统江湖成为异步的终极解决方案(配合async/await),它的价值绝对是不可估量的,值得你亲手实现不止一遍。极简版Promise
需求分析:
万丈高楼平地起,一砖一瓦靠自己。我们先从最简单的核心功能开始,第一步仅实现Promise的构造器函数和then方法 两个功能
功能清单:
- Promise 构造器函数,传入一个函数,该函数立即执行,并且有resolve和reject两个参数,resolve被调用时Promise状态变为fulfilled
- 实现then方法,传入一个函数,该该数在Promise被fulfilled时执行
代码实现:
class PromiseA {
constructor(init) {
this.PromiseStatus = 'pending';
var resolve=(val)=>{
if(this.resolveCallback){
this.PromiseStatus="fulfilled"
this.resolveCallback(val);
}
}
if(init){
init(resolve,reject);
}
}
then(onFulfill,onReject) {
this.resolveCallback=onFulfill;
this.rejectCallback=onReject;
return this;
}
}
复制代码
就这么简单,花几分钟就可以写好,写个测试代码跑一下
new PromiseA(function (resolve){
setTimeout(function (){
resolve("hello,from promise 1");
},2000)
}).then(function (msg){
console.log(msg);
})
复制代码
两秒后输出了:hello,from promise 1
完美运行,能够转得动的轮子就是好轮子!但是,好像还缺了点什么?毕竟我们想做的是奔驰车的轮子......
完整版Promise
需求分析
上一步我们做出了第一个能运行起来的Promise,但是还缺失一些必备功能,如下:
-
- 每次调用then方法应该返回一个新的Promise对象
-
- then方法支持链式调用,链式调用有两种用法:
-
2.1 then注册的onFulfill函数没有返回值,则之后的then全部依次触发 复制代码
-
2.2 then注册的onFulfill函数返回了新的promise,则等这个新的promise fulfill之后,再触发之后的then 复制代码
-
- then 方法需要支持 调用多次
-
- reject函数,以及catch方法
实现思路:
精简版的Promise 很容易实现和读懂,但是链式调用要怎么实现是个难题。
链式调用的两种用法,一种是在onFulfill中不返回promise对象的,自动生成一个promise 对象,并在onfulfill函数return 后自动resolve,这样就会直接触发下一级的then,后面的onfulfill同理,会一直往后触发下去,这样就实现了同一时间依次调用后续的所有then
另一种是在在onFulfill中返回promise对象的,会替换自动生成的promise对象,新的promise对象resolve后, 触发下一个then
两种用法的本质其实是一样的,区别就在于then中新生成的promise会不会自动reslove
链式调用搞清楚了,但是实现起来就有点烧脑了,因为链式调用本身是链表的数据结构,又是高阶函数传来传去,很容易绕晕,我是花了很久时间调试修改,实现思路也是在调试过程中才慢慢理清的,虽然只有几行代码,但是用语言描述比较晦涩难懂,你非得单步调试一下才能明白其中的奥妙。主要实现思路如下:
-
首先,在then方法中返回一个新的promise不是什么难事,new一下就可以了,但是then方法如果返回了promise,要用新的promise替代,问题是then中的promise已经先返回了,这是先有鸡还是先有蛋的问题,时光不能倒流,那只有通过引用传递,改写之前返回的promise了,其实也不用完全替换,只需要改写原有promise的resolve,reject 就可以了。
-
对于then方法注册的回调函数中不返回promise的情况 ,因为每个then都返回了新的promise,当前的promise 判定没有手工返回新的promise,如果有就自动resolve,并将其返回值做为value值传入resolve,这样就会像多米诺骨牌一样触发下一级then, 如果没有return 值,则相当于 resolve(undefined)。
-
then 方法可以被同一个 promise 调用多次 当 promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调 当 promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调
最开始写的版本用的普通变量保存resolve回调,已经修改为数组,这样才能支持多次调用then
完整代码实现:
class PromiseA {
constructor(init) {
this.PromiseStatus = 'pending';
this.PromiseValue = null;
this.reason = null;
this.resolveCallback = [];
this.rejectCallback = [];
var resolve = (val) => {
setTimeout(()=>{
if(this.PromiseStatus != "pending"){return ;}
this.PromiseValue = val;
this.PromiseStatus = "fulfilled"
if (this.resolveCallback) {
this.resolveCallback.forEach( (resolveFn) => {
if(resolveFn === val) {
return reject(new TypeError('promise and x refer to the same object'));
}
var resolveResult=resolveFn(val);
let next = this.nextPromise;
if (resolveResult instanceof PromiseA) { //then方法返回了新的promise,
resolveResult.resolveCallback = next.resolveCallback;
resolveResult.rejectCallback = next.rejectCallback;
} else { //未返回新的promise,直接用返回值resolve
if (next != undefined && next.resolveCallback && next.resolveCallback != this.resolveCallback) { //非空检测,防重复调用
next.resolve(resolveResult);
}
}
})
}
})
}
var reject = (reason) => {
setTimeout(()=>{
if(this.PromiseStatus != "pending"){return ;}
if (this.rejectCallback) {
this.PromiseStatus = "rejected"
this.reason=reason;
this.rejectCallback.forEach( (rejectFn) => {
rejectFn(reason)
});
}
})
}
this.resolve=resolve;
this.reject=reject;
if (init) {
init(resolve, reject);
}
}
then(onFulfill, onReject) {
if(typeof onFulfill!="function"){
onFulfill=function(value) {return value;};
}
if(typeof onReject!="function"){
onReject=function(err) {throw err;}
}
this.resolveCallback.push(onFulfill);
this.rejectCallback.push(onReject);
var promise = new PromiseA();//创建一个新的promise实例
this.nextPromise = promise;//保存一下新的promise引用,便于链式调用
if (this.PromiseStatus == "fulfilled") { //如果是已经fulfilled的promise,立即执行
this.resolve(this.PromiseValue);
} else if (this.PromiseStatus == "rejected"){
this.reject(this.reason);
}
return promise;
}
catch(onRejected) {
return this.then(null, onRejected);
}
}
复制代码
写个测试用例跑一下:
console.time("timer1");
console.time("timer2");
new PromiseA(function (resolve){
setTimeout(function (){
resolve("hello,from promise 1");
},2000)
}).then(function (msg){
console.log(msg);
console.timeEnd("timer1");
}).then(function (msg){
console.log(msg)
console.timeEnd("timer2");
})
复制代码
运行后,两个then回调在2秒后同时触发,说明第一种链式调用验证成功
再测试一下第二种链式调用,测试代码如下:
console.time("timer1");
console.time("timer2");
new PromiseA(function (resolve){
setTimeout(function (){
resolve("hello,from promise 1");
},2000)
}).then(function (msg){
console.log(msg);
console.timeEnd("timer1");
return new PromiseA(function (resolve){
setTimeout(function (){
resolve("world,from promise 2")
},3000)
})
}).then(function (msg){
console.log(msg)
console.timeEnd("timer2");
})
复制代码
验证成功,在2秒后触发了第一个then回调,并接收到了hello,from promise1的返回值,在5秒后触发了第二个then回调,并接收到了"world,from promise2"的返回值
感谢kscript提出的问题,即不注册then和resolve之后再注册then产生的问题,目前最近的代码已经修正了。
实现Promise.all和Promise.race
Promise的实例功能已经完工了,翻翻看Promise构造器函数上还有两个类方法all和race,其中Promise.all是一个非常有用的功能,可以并发执行多个异步任务,全部成功后再执行resolve,无论是处理多个http并行请求,还是并行执行sql脚本等并行计算任务,都十分方便。
有了上面的PromiseA类基础设施,实现这个功能简直不要太简单。这次要用静态方法,也叫类方法,就是在PromiseA构造器函数上定义的,es6 的class 中定义的方法默认是生成在实例的原型中的,加一个static关键字就可以变为静态方法。
实现思路:
- 生成一个新的Promise。
- 遍历传入的promise数组,依次调用每一个promise的then方法注册回调。
- 在then 回调中把promise返回值push到一个结果数组中,检测结果数组长度与promise数组长度相等时表示所有promise都已经resolve了,再执行总的resolve。
Promise.race则更简单,只有任意一个promise fulfilled就执行总的resolve。
代码如下:
static all(list){
return new PromiseA(function (resolve){
var results=[];
list.forEach((promise)=>{
promise.then((val)=>{
results.push(val);
if(results.length==list.length){
resolve(results);
}
})
})
})
}
复制代码
写好之后运行,可以得到并行的结果,但是还存在一点问题,Promise.all resolve后返回的结果数组顺序不能保证和传入的promise一致, 感谢tk103331的提出,因此还需要改写一下,上面的代码是用push,因此顺序是按照时间顺序往后追加结果的,如果传入的数组下标直接给结果数组赋值,就可以解决此问题。
修改后的代码如下:
static all(list){
return new PromiseA(function (resolve){
var results=new Array(list.length);
var completeCount=0;
list.forEach((promise,index)=>{
promise.then((val)=>{
results[index]=val;
completeCount++;
if(completeCount==list.length){
resolve(results);
}
})
})
})
}
static race(list){
return new PromiseA(function (resolve){
list.forEach((promise)=>{
promise.then((val)=>{
resolve(val);
})
})
});
}
}
复制代码
我们写个测试用例跑一下
console.time("all");
var p1=new PromiseA(function (resolve){
setTimeout(function (){
let msg="promise 1111"
console.log(msg);
resolve(msg);
},5000)
})
var p2=new PromiseA(function (resolve){
setTimeout(function (){
let msg="promise 2222"
console.log(msg);
resolve(msg);
},2000)
})
var p3=new PromiseA(function (resolve){
setTimeout(function (){
let msg="promise 3333"
console.log(msg);
resolve(msg);
},3000)
})
PromiseA.all([p1,p2,p3]).then(function (results){
console.timeEnd("all");
console.log(results)
})
复制代码
执行结果,能够保证顺序了。
小结
盘点了一下,一共用了90多行代码,实现了Promise A+的绝大部分功能,业务上能用到的所有功能都已经实现,但是有一些语法糖和容错处理没有做,尚不能跑通Promise规范的所有用例。目前主流浏览器和node.js都已实现了es6 promise规范,所有第三方的promise库都失去了存在的意义, 本文代码仅限于技术研究,深入理解原理。
最后,推广一下个人的开源项目,node.js web开发框架: webcontext