为什么要用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是什么
Promise
Promise
是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理且更强大。它最早由社区提出并实现,ES6将其写进了语言标准,统一了用法,并原生提供了Promise对象。
同步和异步强调的是消息通信机制 (synchronous communication/ > > asynchronous communication)。所谓同步,就是在发出一个"调用"时,在没有得到结果之前,该“调用”就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由“调用者”主动等待这个“调用”的结果。而异步则是相反,"调用"在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在"调用"发出后,"被调用者"通过状态、通知来通知调用者,或通过回调函数处理这个调用
// 同步场景
function sleep(milliSeconds) {
var startTime = new Date().getTime();
while (new Date().getTime() < startTime + milliSeconds);
}
// 高中食堂打饭
function waitformeal(name,meal) {
console.log(`${name}同学:我要${meal}`)
sleep(10000);
console.log(`食堂大妈:${meal}好了,同学`)
console.log(`${name}同学开始干饭...`)
console.log("....................")
}
waitformeal("赵","麻辣烫")
waitformeal("钱","黄焖鸡")
waitformeal("孙","盖浇饭")
//异步场景 麦当劳叫号模式
function waitformeal(name,meal) {
console.log(`${name}同学:我要${meal}`)
setTimeout(()=>{
console.log(`麦当劳小姐姐:${meal}好了,同学`)
console.log(`${name}同学开始干饭...`)
console.log("....................")
},10000)
console.log(`${name}同学,干其他事情去了`)
}
waitformeal("赵","香辣鸡腿堡套餐")
waitformeal("钱","全家桶")
waitformeal("孙","两份全家桶")
console.log(`后厨准备中......`)
特点
- 对象的状态不受外界影响 (3种状态)
* Pending状态(进行中)
* Fulfilled状态(已成功)
* Rejected状态(已失败)
- 一旦状态改变就不会再变 (两种状态改变:成功或失败)
* Pending -> Fulfilled
* Pending -> Rejected
用法
创建Promise实例
__
var promise = new Promise(function(resolve, reject){
// ... some code
if (/* 异步操作成功 */) {
resolve(value);
} else {
reject(error);
}
})
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由JavaScript引擎提供,不用自己部署。
resolve作用是将Promise对象状态由“未完成”变为“成功”,也就是Pending -> Fulfilled
,在异步操作成功时调用,并将异步操作的结果作为参数传递出去;而reject函数则是将Promise对象状态由“未完成”变为“失败”,也就是Pending -> Rejected
,在异步操作失败时调用,并将异步操作的结果作为参数传递出去。
then
Promise实例生成后,可用then
方法分别指定两种状态回调参数。then 方法可以接受两个回调函数作为参数:
- Promise对象状态改为Resolved时调用 (必选)
- Promise对象状态改为Rejected时调用 (可选)
基本用法示例
__
function sleep(ms) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, ms);
})
}
sleep(500).then( ()=> console.log("finished"));
这段代码定义了一个函数sleep,调用后,等待了指定参数(500)毫秒后执行then中的函数。值得注意的是,Promise新建后就会立即执行。
执行顺序
接下来我们探究一下它的执行顺序,看以下代码:
__
let promise = new Promise(function(resolve, reject){
console.log("AAA");
resolve()
});
promise.then(() => console.log("BBB"));
console.log("CCC")
// AAA
// CCC
// BBB
执行后,我们发现输出顺序总是 AAA -> CCC -> BBB
。表明,在Promise新建后会立即执行,所以首先输出 AAA
。然后,then方法指定的回调函数将在当前脚本所有同步任务执行完后才会执行,所以BBB 最后输出
。
与定时器混用
首先看一个实例:
__
let promise = new Promise(function(resolve, reject){
console.log("1");
resolve();
});
setTimeout(()=>console.log("2"), 0);
promise.then(() => console.log("3"));
console.log("4");
// 1
// 4
// 3
// 2
可以看到,结果输出顺序总是:1 -> 4 -> 3 -> 2
。1与4的顺序不必再说,而2与3先输出Promise的then,而后输出定时器任务。原因则是Promise属于JavaScript引擎内部任务,而setTimeout则是浏览器API,而引擎内部任务优先级高于浏览器API任务,所以有此结果。
Promise优缺点
优点 | 缺点 |
---|---|
解决回调 | 无法监测进行状态 |
链式调用 | 新建立即执行且无法取消 |
减少嵌套 | 内部错误无法抛出 |
解决上面遗留的问题
刚才麦当劳小姐姐的异步可以写成这样:
function haveMeal(name,meal) {
return new Promise((resolve => {
console.log(`${name}同学:我要${meal}`)
setTimeout(()=>{
resolve({name,meal})
},10000)
console.log(`${name}同学,干其他事情去了`)
}))
}
function mealIsOk(data){
let {name,meal} = data
console.log(`麦当劳小姐姐:${meal}好了,同学`)
console.log(`${name}同学开始干饭...`)
console.log("....................")
}
haveMeal("赵","香辣鸡腿堡套餐").then(mealIsOk)
haveMeal("钱","全家桶").then(mealIsOk)
haveMeal("孙","两份全家桶").then(mealIsOk)
console.log(`后厨准备中......`)
上面电影网站的例子,改成下面的写法:
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入参方法。
结果:
开始执行定时器
只是执行了日志,并没有看到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对象,继续下一次操作。
开始执行定时器:1
我是回调方法
执行回调方法1
开始执行定时器:2
我是回调方法
执行回调方法2
开始执行定时器:3
我是回调方法
执行回调方法3
回过头来看我们开篇讲到海报加载的例子,到此可以理解了。
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);
});
执行的结果:
开始执行定时器:1
执行回调方法失败:1
执行回调方法:undefined
开始执行定时器:3
执行回调方法失败:3
第一次执行时,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);
})
执行的结果如下:
开始执行定时器:1
执行回调方法:1
ReferenceError:x is not defined
捕获并打印了异常。
我们来解决前面提的问题,利用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{
reject(num);
}
},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
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);
})
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的基本知识,在实际项目,有很多已经分装好的库可以使用,如jquery,微信小程序等,万变不离其踪,只要了解了基本的原理,这些库使用起来也会得心用手。