1 背景
由于JavaScript是单线程语言,所以所有的网络请求、浏览器事件、自定义延时事件为了不阻塞主线程,都必须是异步执行的,异步执行可以用回调函数来实现。
setTimeout(function () {
console.log('success'); //1秒钟后打印success
}, 1000);
复制代码
对于简单的情况,回调函数有着不错的表现。
但是,如果情况变得复杂了呢?比如说一个延时事件必须等到另一个延时事件结束后才能执行。
setTimeout(function () {
console.log('success'); //1秒钟后打印success
setTimeout(function () {
console.log('again success'); //2秒钟后打印again success
}, 1000);
}, 1000);
复制代码
此时代码结构开始变得复杂起来。随着复杂程度进一步的增加,回调函数的嵌套层数会越来越多,最终形成回调金字塔,使的代码变得难以阅读。
Promise则正是为了解决这一问题而出现的。
2 创建一个Promise对象
Promise被用来处理异步事件,当异步事件执行完成后,会根据成功或失败的结果,进行后续的逻辑操作。
首先,根据语法:
new Promise( function(resolve, reject) {...} /* executor */ );
我们来创建一个Promise对象
function executor(resolve,reject){
//todo async event
}
var promise = new Promise(executor);
复制代码
executor函数是我们用来处理异步事件的,它接收两个参数resolve和reject。
当我们创建一个Promise对象(new Promise)的时候,Promise会立刻执行executor函数,并且把resolve和reject两个函数作为参数传递给executor函数。
在此之后,Promise会返回一个对象,我们用变量promise来接收这个对象。
3 使用Promise对象
3.1 resolve和reject
function executor(resolve,reject){
setTimeout(function () {
if( isSuccess() ){ //isSuccess()是一个能够随机返回true或false的函数,目的是为了模拟异步事件是否执行成功
resolve('success')
}else{
reject('run reject')
}
},1000)
}
var promise = new Promise(executor);
复制代码
我们知道,executor是我们用来处理异步事件的,那么无论这个异步事件是否执行成功,总要发出一个信号,告诉我们的promise对象异步事件已经结束,可以进行下一步的操作了。
resolve和reject就是这个信号的发起者了。
- 如果异步事件执行成功,那么就调用resolve函数,并把成功的结果(例如字符串success)作为参数传递进去。
- 如果异步事件执行失败,那么就调用reject函数,并把失败的结果(例如字符串fail)作为参数传递进去。
3.2 .then和.catch
function executor(resolve,reject){
setTimeout(function () {
if( isSuccess() ){ //isSuccess()是一个能够随机返回true或false的函数,目的是为了模拟异步事件是否执行成功
resolve('success')
}else{
reject('fail')
}
},1000)
}
var promise = new Promise(executor);
promise.then(function(res){
console.log('执行成功:',res) //1秒钟后,如果异步事件执行成功,调用了resolve函数,则会打印执行成功:success
});
promise.catch(function(err){
console.log('执行失败:',err) //1秒钟后,如果异步事件执行失败,调用了reject函数,则会打印执行失败:fail
});
复制代码
与resolve和reject对应的。.then和.catch就是信号的接收者了。
- 当executor函数调用resolve方法时,就会执行.then里面的回调函数,成功的结果(字符串success)会作为参数传递给该回调函数。
- 当executor函数调用reject方法时,就会执行.catch里面的回调函数,失败的结果(字符串fail)会作为参数传递给该回调函数。
.then和.catch里面的回调函数可以根据不同的异步事件执行结果进行不同的后续操作。
3.3 .then和.catch回调函数的特性
.then和.catch的回调函数并不会立即执行,而是会被注册到一个任务队列里,且同时具有以下3种特性:
- 只有同时满足异步事件执行完成且JavaScript主线程执行完毕这两个条件,才会执行任务队列里的函数。
- 即使异步事件已经结束了,向任务队列中注册的回调函数依然会被正确的执行。
- 通过多次调用.then向任务队列注册多个回调函数,它们会按照注册的顺序独立的运行。(注意,.catch并不具备此特性)
console.log('main start');
let promise = new Promise(function(resolve){
setTimeout(function () {
resolve('success')
},1000)
});
promise.then(function(res){
console.log('执行成功:',res);
});
setTimeout(function () {
promise.then(function(res){
console.log('执行成功2:',res);
});
},2000);
console.log('main end');
//执行顺序如下:
//main start
//main end
//(1秒钟后)执行成功:success
//(2秒钟后,此时异步事件已经结束了,但函数依然能被正确的执行)执行成功2:success
复制代码
3.4 小结
至此,一个Promise流程——从异步事件发起(new Promise(executor)),到异步事件结束(resolve或reject),再到根据不同的结果执行不同的操作(.then或.catch),就完整的结束了。
可以看出,与传统的回调函数相比,使用Promise结构会把异步事件代码和处理结果代码分离开来,不仅利于阅读,还有助于代码的复用。
在写法上,由于:
- 只使用一次的函数可以写成匿名函数。所以可以把executor函数写成匿名函数的形式。
- new Promise和.then以及.catch都会返回一个Promise对象。所以可以把.then和.catch写成链式调用的形式。
对Promise的应用也可以写成以下形式。
new Promise(function(resolve,reject){
setTimeout(function () {
if( isSuccess() ){
resolve('success')
}else{
reject('fail')
}
},1000)
}).then(function(res){
console.log('执行成功:',res)
}).catch(function(err){
console.log('执行失败:',err)
});
复制代码
4 Promise方法
4.1 Promise.all()
有时候我们需要一次性的执行多个异步事件,并且需要在这些异步事件全部执行成功后再进行后续的操作。
这时候Promise.all()就派上了大用场。
var promise1 = new Promise(function(resolve,reject){
setTimeout(function () {
if( isSuccess() ){
resolve('promise1 success')
}else{
reject('promise1 fail')
}
},1000)
})
var promise2 = new Promise(function(resolve,reject){
setTimeout(function () {
if( isSuccess() ){
resolve('promise2 success')
}else{
reject('promise2 fail')
}
},2000)
})
var promiseList = [];
promiseList.push(promise1);
promiseList.push(promise2);
Promise.all(promiseList).then(resList=>{
console.log('执行成功:',resList) //如果promise1和promise2都执行成功,那么2秒后会打印 执行成功:['promise1 success','promise2 success'] 。除此之外,都会执行.catch的回调函数。
}).catch(err=>{
console.log('执行失败:',err) //根据失败的来源,打印结果有两个 执行失败:promise1 fail 或者 执行失败:promise2 fail
});
复制代码
Promise.all()接收一个数组,该数组的元素是promise对象。Promise.all()会等待数组里面所有的promise都执行成功,或者第一个执行失败。
- 当所有的promise都执行成功时,就会调用.then的回调函数,并把所有的成功结果按照顺序放到一个数组里,作为参数传递给该回调函数。也就是说,promise在数组中的顺序与成功结果在数组中的顺序是一致的。
- 当第一个promise执行失败时,就会调用.catch的回调函数,并把该promise的失败结果作为参数传递给该回调函数。
4.2 Promise.race()
有时候我们需要一次性的执行多个异步事件,并且当第一个异步事件执行成功或失败就进行后续的操作,而不关心剩余异步事件的执行结果。
这时候就可以使用Promise.race()来解决这一类问题。
var promise1 = new Promise(function(resolve,reject){
setTimeout(function () {
if( isSuccess() ){
resolve('promise1 success')
}else{
reject('promise1 fail')
}
},1000)
})
var promise2 = new Promise(function(resolve,reject){
setTimeout(function () {
if( isSuccess() ){
resolve('promise2 success')
}else{
reject('promise2 fail')
}
},2000)
})
var promiseList = [];
promiseList.push(promise1);
promiseList.push(promise2);
Promise.race(promiseList).then(res=>{
console.log('执行成功:',res) //当第一个promise执行成功时,打印该promise成功结果 执行成功:promise1 success 或者 执行成功:promise2 success
}).catch(err=>{
console.log('执行失败:',err) //当第一个promise执行失败时,打印该promise失败结果 执行失败:promise1 fail 或者 执行失败:promise2 fail
});
复制代码
Promise.all()接收一个数组,该数组的元素是promise对象。Promise.race()会等待数组里面的第一个promise执行成功或者执行失败。
- 当第一个promise执行成功时,就会调用.then的回调函数,并把该promise的成功结果作为参数传递给该回调函数。
- 当第一个promise执行失败时,就会调用.catch的回调函数,并把该promise的失败结果作为参数传递给该回调函数。
4.3 Promise.resolve()
Promise.resolve()会直接返回一个执行成功的Promise对象。
Promise.resolve('success').then(function(res) {
console.log('执行成功:',res); //直接打印 执行成功:success
});
复制代码
我们一般很少用到这个方法,但是这个方法解释了为什么.then和.catch能够链式调用。
上文说到过:
new Promise和.then以及.catch都会返回一个Promise对象。所以可以把.then和.catch写成链式调用的形式。
new Promise(function(resolve,reject){
setTimeout(function () {
if( isSuccess() ){
resolve('success')
}else{
reject('fail')
}
},1000)
}).then(function(res){
console.log('执行成功:',res)
}).catch(function(err){
console.log('执行失败:',err)
});
复制代码
首先,Promise作为一个类,new Promise的时候返回一个对象,这没问题(详情请参见ES6 class)。
但是.then和.catch为什么也会返回一个promise对象呢?
这是因为在JavaScript中,函数都是有返回值的,如果没有返回值,那么函数会隐式的(自动的)返回一个undefined。
而.then和.catch又会把函数的返回值隐式的(自动的)用Promise.resolve()包装一下。
所以,.then和.catch就成功的返回了一个promise对象,也就能实现链式调用的书写结构了。
而这个链式调用的特性同时解决了回调函数的回调金字塔问题。
new Promise(function(resolve){
setTimeout(function () {
resolve('success')
},1000)
}).then(function(res){
console.log(res); //1秒钟后打印 success
return new Promise(function(resolve){
setTimeout(function(){
resolve('again success')
},1000)
})
}).then(function(res){
console.log(res); //2秒钟后打印 again success
});
复制代码
对比多层嵌套的函数回调结构,链式调用结构清晰,更利于阅读和维护。
4.4 Promise.reject()
Promise.reject()会直接返回一个执行失败的Promise对象。
Promise.reject('fail').catch(function (err) {
console.log('执行失败:',err) //直接打印 执行失败:fail
});
复制代码
这个方法也很少用到。但值得一提的是,.catch除了能响应reject之外,也能响应在promise流程中的异常抛出事件。
Promise.reject('fail').catch(function (err) {
console.log('执行失败:',err); //打印结果 执行失败:fail
throw 'throw fail'
}).catch(function (err) {
console.log('执行失败:',err); //打印结果 执行失败:again fail
});
复制代码
5 总结
Promise作为处理异步事件的解决方案。
- 很好的把异步事件代码和处理结果代码分离开来。
- 提供了all和race方法来解决一次性执行多个异步事件这一类问题。
- 其链式调用的特性很好的解决了函数回调的问题。
灵活的运用Promise将会为我们的开发工作提供很大的便利。