一、为什么要用Promise
在实际项目中,有下面应用场景,在手机上浏览电影网站,其中有个海报墙页面,里面有海量的电影节目,如下图所示。考虑到性能和用户体验,启动后,我们需要串行的加载10页数据(每页9张海报),即第一页加载完成后,启动第二页的加载,以此类推。
于是不假思索的写下了下面的代码:
$(document).ready(function(){
//获取第一页数据
$.getJSON("json/poster.json?page=1",function(result){
attachPoster(result);
//获取第二页数据
$.getJSON("json/poster.json?page=2",function(result){
attachPoster(result);
//获取第三页数据
$.getJSON("json/poster.json?page=3",function(result){
attachPoster(result);
...
});
});
});
});
一直写到自己恶心,这就是叫做"回调地狱"。
是否有解?有,那就是Promise
二、Promise是什么
还用上面的例子,改成下面的写法:
function getPoster(page){
const promise = new Promise(function(resolve,reject){
$.getJSON("json/poster.json?page="+page,function(result){
resolve(result);
})
});
return promise;
}
getPoster(1).then(function(result){//获取第一页
attachPoster(result);
return getPoster(2);
}).then(function(result){//获取二页
attachPoster(result);
return getPoster(3);
}).then(funciton(result){//获取第三页 ...})
道友们先不用理会Promise的细节,从代码结构上看,比第一种的层层嵌套是不是更清晰,更符合逻辑。Promise就是为了解决回调函数嵌套的一种解决方案。
三、then与resolve
通过下面例子初步了解Promise是怎么玩的。
function getPObj(){
var p = new Promise(function(resolve,reject){
setTimeout(function(){
console.log("开始执行定时器");
resolve("执行回调方法");
},2000);
});
return p;
}
getPObj();
在getPObj中我们new了Promise对象p并返回该对象,在构造p对象的方法中,只有一个定时器,2s钟后打印一个日志和执行resolve入参方法。
我们调用getPObj执行下,结果如下:
只是执行了日记,并没有看到resolve方法的执行,这也不奇怪,因为resolve作为构造函数的入参,我们根本就没有定义。
我们将代码改成下面
function getPObj(){
var p = new Promise(function(resolve,reject){
setTimeout(function(){
console.log("开始执行定时器");
resolve("执行回调方法");
},2000);
});
return p;
}
getPObj().then(function(data){
console.log("我是回调方法");
console.log(data);
});
再次此时的执行结果:
then的入参函数,就是resovle的回调方法。看到这里,大家可能会问,这不就是个callback作为入参的回调么,只不过用了then的属性方法传入的,一种表示方式而已,有啥稀奇的。如果只是一层嵌套是看不出优越性,还记得我们前面海报加载的场景么,如果嵌套多层,then的链式调用就发挥巨大优先性了,它能把层层嵌套平铺开来。
我们将上面的实例再改造下:
function getPObj(num){
var p = new Promise(function(resolve,reject){
setTimeout(function(){
console.log("开始执行定时器:"+num);
resolve(num);
},2000);
});
return p;
}
getPObj(1).then(function(data){
console.log("我是回调方法");
console.log("执行回调方法:"+data);
return getPObj(2);
}).then(function(data){
console.log("我是回调方法");
console.log("执行回调方法:"+data);
return getPObj(3);
}).then(function(data){
console.log("我是回调方法");
console.log("执行回调方法:"+data);
});
在每个回调执行完成后,再返回一个新的Promise对象,继续下一次操作。
回过头来看我们开篇讲到海报加载的例子,到此可以理解了。
四、reject
细心的道友可能发现,在Promise对象的构造方法的入参中,还有个reject方法我们还没有讲到。
const promise = new Promise(function(resolve,reject){
somethingDO();
if (/*结果符合预期,异步操作成功*/) {
resolve()
}else/*不符合预期,操作失败*/
{
reject();
}
})
Pomise有三种状态,分别是pending(进行中),resolved(已成功),rejected(已失败),一旦达到相应的状态,就会回调相应的方法。其实称作已成功,或者已失败并不准确,ES6中标准说法fullfiled,rejected。至于什么是已成功状态,什么是已失败状态,可以自己按照实际情况自定义。
对应的,then方法有两个入参,分别实现resolved,rejected的回调方法。
promise.then(function(value) {
// resolved
}, function(error) {
// rejected
});
继续上面的实例,我们在方法增加控制,生成一个1-10的随机方法,如果大于5就表示失败。
function getPObj(num){
var p = new Promise(function(resolve,reject){
setTimeout(function(){
console.log("开始执行定时器:"+num);
var i = Math.ceil(Math.random()*10); //生成1-10的随机数
if (i<5) {
resolve(num);
}else{
reject(num);
}
},2000);
});
return p;
}
getPObj(1).then(function(data){
console.log("执行回调方法:"+data);
return getPObj(2);
},function(data){
console.log("执行回调方法失败:"+data);
}).then(function(data){
console.log("执行回调方法:"+data);
return getPObj(3);
},function(data){
console.log("执行回调方法失败:"+data);
}).then(function(data){
console.log("执行回调方法:"+data);
},function(data){
console.log("执行回调方法失败:"+data);
});
执行的结果:
第一次执行时,i的随机值就大于5,所以执行了rejected的方法。但是和我们预期的还是有点不一样,如果返回失败,我们希望终止掉整个链条,但是从实际结果看,是继续往下执行。这是因为,回调第一个reject的方法后,没有返回值,Promise会自动返回一个undefined,传入下一个链条的resolve方法中,并继续后面的then链。
有没有方法,一旦执行失败,就中断后面的then链条呢?有,各位继续往下。
五、catch
try...catch我们常用捕获异常的方法,在promise对象中也有catch的方法。用来捕获then回调方法中抛出的各类异常,用法如下:
p.then(function(){
...
}).catch(e){
....
}
现在我们用上面的实例构造一个异常。
function getPObj(num){
var p = new Promise(function(resolve,reject){
setTimeout(function(){
console.log("开始执行定时器:"+num);
var i = Math.ceil(Math.random()*10); //生成1-10的随机数
if (i<5) {
resolve(num);
}else{
reject(num);
}
},2000);
});
return p;
}
getPObj(1).then(function(data){
console.log("执行回调方法:"+data);
//x没有定义,抛出异常
x+2;
}).catch(function(e){
console.log(e);
})
执行的结果如下:
捕获并打印了异常。
我们来解答第四章节提的问题,利用catch可终止then链条。如下
function getPObj(num){
var p = new Promise(function(resolve,reject){
setTimeout(function(){
console.log("开始执行定时器:"+num);
var i = Math.ceil(Math.random()*10); //生成1-10的随机数
if (i<5) {
resolve(num);
}else{
//构造一个error作为入参
reject(new Error("Error"));
}
},2000);
});
return p;
}
getPObj(1).then(function(data){
console.log("执行回调方法:"+data);
return getPObj(2);
}).then(function(data){
console.log("执行回调方法:"+data);
return getPObj(3);
}).then(function(data){
console.log("执行回调方法:"+data);
}).catch(function(e){
console.log(e);
});
reject构造一个error的入参,抛出异常,为catch捕获。从执行结果看,后面的then的没有执行,达到目的。
道友们看到这个实例中,每个then的reject的方法都删除了,catch方法实际就是实现了全局的reject方法。在实际开发中,我们建议采用catch代替reject。
六、finally
try...catch...finally是黄金组合,做过java开发的道友们对肯定非常熟悉。finally表示无论什么状态,必定都会执行。
function getPObj(num){
var p = new Promise(function(resolve,reject){
setTimeout(function(){
console.log("开始执行定时器:"+num);
var i = Math.ceil(Math.random()*10); //生成1-10的随机数
if (i<5) {
resolve(num);
}else{
reject(new Error("出错了"));
}
},2000);
});
return p;
}
getPObj(1).then(function(data){
console.log("执行回调方法:"+data);
return getPObj(2);
}).then(function(data){
console.log("执行回调方法:"+data);
return getPObj(3);
}).then(function(data){
console.log("执行回调方法:"+data);
}).catch(function(e){
console.log(e);
}).finally(function(){
console.log("finally");
});
执行结果:
七、all、race
1、all
Promise.all可以将几个Promise对象封装成一个,格式如下:
Promise.all([p1,p2,p3]).then(function(data){...})
当这几个对象都变成resolved状态后,总状态变为resolved;否则,其中有一个为rejected状态,则变成reject,其他的可以忽略。可以理解为p1&&p2&&p3。
那返回的data是什么样子,如果是resolved状态,则是各个对象data的组合;如果是rejected,则是第一个到达rejected状态返回的data值。以例为证。
都为resolved状态:
function getPObj(num){
var p = new Promise(function(resolve,reject){
setTimeout(function(){
console.log("开始执行定时器:"+num);
resolve(num);
},2000);
});
return p;
}
Promise.all([getPObj(1),getPObj(2),getPObj(3)]).then(function(data){
console.log("resolve");
console.log(data);
}).catch(function(e){
console.log("error");
console.log(e);
})
其中有一个返回rejected状态:
function getPObj(num){
var p = new Promise(function(resolve,reject){
setTimeout(function(){
console.log("开始执行定时器:"+num);
var i = Math.ceil(Math.random()*10); //生成1-10的随机数
if (i<5) {
resolve(num);
}else{
reject(num);
}
},2000);
});
return p;
}
Promise.all([getPObj(1),getPObj(2),getPObj(3)]).then(function(data){
console.log("resolve");
console.log(data);
}).catch(function(e){
console.log("error");
console.log(e);
})
执行结果:
2、race
race与all类似,页可以将几个Promise对象封装成一个,格式如下:
Promise.race([p1,p2,p3]).then(function(data){...})
不同的时,看谁执行的快,then就回到回调谁的结果。可以理解为p1||p2||p3
看实例:
function getPObj(num){
var p = new Promise(function(resolve,reject){
setTimeout(function(){
console.log("开始执行定时器:"+num);
var i = Math.ceil(Math.random()*10); //生成1-10的随机数
if (i<5) {
resolve(num);
}else{
reject(num);
}
},2000);
});
return p;
}
Promise.race([getPObj(1),getPObj(2),getPObj(3)]).then(function(data){
console.log("resolve");
console.log(data);
}).catch(function(e){
console.log("error");
console.log(e);
})
执行结果(由于随机数,各位执行的结果会有不一样的情况):
当接受到第一个对象的resolved状态后,其他的两个抛弃处理。
八、总结
本文主要阐述了Promise的基本知识,在实际项目,有很多已经分装好的库可以使用,如q.js,when.js,jquery.deferred等,万变不离其踪,只要了解了基本的原理,这些库使用起来也会得心用手。
下一篇:ES6系列教程第二篇--Iterator 详解