最近有ajax之类异步编程需求,整理下promise的使用。
背景介绍
Js是单线程语言,为了避免耗时任务阻塞代码,对耗时长的任务采用异步模式调用。前一个任务执行完,调用回调函数而不是进行后一个任务。后一个任务不等前一个任务结束就执行,任务调用的顺序和排列顺序不同。
实现异步和后续处理最原始的方法就是使用回调函数,在耗时任务结束时调用后续操作。
示例:
function f(callback){
//setTimeout模拟耗时操作
setTimeout(function(){
console.log("test")
callback()
},1000)
}
function f2(){
console.log("callback")
}
f(f2)
示例中简单把函数f2作为回调函数传入函数f中,利用f(f2)
实现异步回调操作。但回调方法一旦变得复杂,就会出现多重回调的情况,代码容易变得庞大复杂。比如项目中用到不同的接口进行ajax调用,如下面示例:
$.ajax({
//请求获取场地信息
url: '/position',
success (result){
let pId = result.filter(
item => {
if(item.posName == 'north'){
return item
}
}
)[0].posId
//请求获取场地对应的渔场信息
$.ajax({
url: '/place?pid=' + pId,
success (result){
//获取该场地第一块渔场信息
let placeId = result[0].placeId
$.ajax({
//获取对应渔场鱼种信息
url: '/fish?pid=' + pId +"&placeid=" + placeId,
success (result){
result.forEach(element => {
console.log(element.name)
});
}
})
}
})
}
})
多重回调导致代码可读性非常差,为了更方便地进行异步编程地实现,ES6中实现了promise对象来解决问题。
promise对象介绍
Promise是一个对象,它支持函数return,异步操作不需要再层层传递回调函数进行回,同时能合并多个异步等待操作,便于处理。
Promise有状态的概念,其有三种状态:pending(运行),resolved(成功)和rejected(失败)
。状态一经改变则不会再改变。改变的模式有两种,分别为pending->resolved和pending->rejected。简单来说,promise就是能用同步方式写异步代码。
Promise对象的构造函数参数为执行任务操作的函数,简单示例:
var p = new Promise(
function(resolve, reject){
console.log("start")
setTimeout(
function(){
console.log("step 1")
resolve("step 1 complete")
},1000
)
}
可以看到,参数函数中包括两个参数,分别为resolve(fulfille)和reject方法。当调用resolve方法后,该promise对象的状态就变为resolved。同样,当调用reject方法后,该promise对象的状态变为rejected。
Promise中的常用方法有then,catch,all,race等,下面来分别讲下作用。
then
Then()用来形成promise队列,是promise实现异步操作同步写法的重要手段。
首先来看下这几个函数:
function start(){
var p = new Promise(
function(resolve, reject){
console.log("start")
setTimeout(
function(){
console.log("step 1")
resolve("step 1 complete")
},1000
)
}
)
return p
}
function second(result){
console.log(result)
var p = new Promise(
function(resolve, reject){
setTimeout(
function(){
console.log("step 2")
resolve("step 2 complete")
},1000
)
}
)
return p
}
function end(result){
console.log(result)
var p = new Promise(
function(resolve, reject){
setTimeout(
function(){
console.log("all step complete")
resolve("end")
},1000
)
}
)
return p
}
可以看到,每个函数中都有一个延时异步操作的promise,并返回对应的promise对象。那么如果需要将这三个操作连接使用,start->second->end顺序来执行,就需要使用promise的then方法进行处理。
function promise1(){
start()
.then(second)
.then(end)
.then(function(result){
console.log(result)
})
}
当利用then形成promise链时,前一个promise调用会传递一个promise对象到后续的then中。Then可以有两个参数,一个为resolve方法,一个为reject方法。这两个方法调用哪个,由之前传递过来的promise的状态指定。promise中调用resolve或reject方法时传递的参数,会作为下一级then中调用函数的参数来使用。
在上述示例中,start中return promise对象,那么该promise执行resolve方法后,状态改变,自身被传递到下一个then中执行resolve方法,也就是second。以此类推,最终输出所有信息顺序为:
Start
Step1
Step1complete
Step2
Step2complete
all step complete
end
上面的函数都传递了promise对象给下一级then,那么如果return别的值呢?
如果return的值为promise对象,那么下一级then会在其状态发生改变后执行对应的方法。
如果return的值为其它对象,那么下一级then则会立即执行,同时遵循的promise状态为上一个存在的被传递过来的promise的状态,并一直传递下去,直到函数返回新的promise对象。
对于直接执行then的理解,可以看下面这个示例:
function f_1(){
var p = new Promise(function(resolve, reject){
setTimeout(function() {
console.log("first")
resolve("second");
}, 1000);
})
return p
}
f_1()
.then(function(data){
setTimeout(function(){
console.log(data)
},1000)
return "end"
})
.then(function(data){
console.log(data)
})
该示例的输出结果为:
first
end
second
分析一下,第一个promise延时方法输出first执行resolve改变状态后,下一级then执行函数,而此时,then中函数返回的不是promise,那么再下一级then就立刻执行,输出end。最后前一个then中延时方法执行,输出second。
这个例子很好地说明了两种不同返回情况对于后续then的执行的差异,看懂了基本就没问题了。
catch
catch主要有两种使用方法:用来代替reject的回调和捕获异常并处理。
代替reject回调的示例:
function promise4(){
var p = new Promise(
function(resolve, reject){
setTimeout(
function(){
reject("rejected")
},1000
)
}
)
p
.then((result => {console.log("good1")}))
.catch((() => {console.log("bad2")}))
}
示例最终输出为bad2。可以看到,then中没有指定reject的方法,调用了catch中方法进行处理。
捕获异常的演示:
function promise5(){
var p = new Promise(
function(resolve, reject){
setTimeout(
function(){
resolve("good")
},1000
)
}
)
p
.then((result => {
throw new Error("no good")
console.log("good1")
}))
.catch((result => {console.log(result)}))
}
输出为
Error: no good
at promise_test.js:137
成功捕获到异常
需要注意的是,在默认情况下,catch会返回一个resolved状态的promise对象
。也就是说,如果catch后还有下一级then,它也会正常进行操作。而当catch中也抛出错误时,会直接定位到链中的下一个catch执行,略过中间的操作。
all
All的使用也是promise的一大创新点,它能同时进行数个promise的执行,并且将所有的返回通过数组形式整合后返回。形式为Promise.all([p1, p2, p3])
示例:
function a_1(){
var p = new Promise(function(resolve, reject){
setTimeout(function() {
resolve("a1");
}, 1000);
})
return p
}
function a_2(){
var p = new Promise(function(resolve, reject){
setTimeout(function() {
resolve("a2");
}, 1000);
})
return p
}
Promise
.all([a_1(), a_2()])
.then(result => console.log(result))
输出为[“a1”,”a2”]。
All的调用会在所有异步操作结束后返回。
race
race方法使用和all差不多,但是该方法在任意一个延时操作完成后就返回,所以将有时间限制的方法和计时器进行绑定,进行超时提示。
resolve和reject
这两个为Promise的静态方法,分别用来返回resolved状态的promise实例和reject状态的promise实例。参数value作为对应的返回值。
其中resolved方法的参数也可为promise对象,此时就返回该对象。为拥有函数then的thenable对象时,返回的对象状态由这个thenable对象决定。
之前的示例中,我们都是由一个返回promise对象的函数起手,有了这两个方法就可以直接获得想要的状态的promise对象,很方便。
总结
讲到这里,前面背景中提出的多重回调方法就能完美解决了。在实际项目中,ajax的调用和promise结合后就会方便很多。同时all,race等,解决了超时和同时异步操作的问题。ES7中增加的async/await关键词,个人认为就是对promise的一种编写方法上的延展,让异步代码看起来更像同步代码,理解起来反而增加难度,个人不太喜欢。