本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
由于JavaScript是单线程的。在处理网络操作、事件操作时都是需要进行异步执行的。AJAX就是一个典型的异步操作
对于异步操作,有传统的利用回调函数和使用 Promise,二者的对比如下:
// 传统回调方式
函数1(function(){
//代码执行...(ajax1)
函数2(function(){
//代码执行...(ajax2)
函数3(function(data3){
//代码执行...(ajax3)
});
...
});
});
//Promise回调方式:链式调用,可构建多个回调函数。
//例如请求一个ajax之后,需要这个拿到这个ajax的数据去请求下一个ajax
promise().then().then()...catch()
对比可知,使用传统回调函数方式处理异步操作很复杂。为了解决这样的问题,commonJS引入了Promise概念,很好的解决了JavaScript的异步操作。
概念
Promise是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更简单易理解且实用,所以Promise简单来说就是一个容器,里面保存着某个未来才会执行事件(通常为一个异步操作)的结果。
特点
- 对象的状态不受外界影响
- 一旦状态改变,就不会再变化,任何时候都可以得到这个结果
语法
//创建Promise实例
let promise = new Promise( (resolve,reject) =>{
// 执行相应代码并根据情况调用resolve或者reject
...
} )
// 在promise的then方法中执行回调
Promise.then( function(){
// 第一个参数是返回resolve状态时执行的回调函数
},function(){
// 第二个参数是返回reject状态时执行的回调函数
} )
状态
Promise对象代表一个异步操作,有三种状态:
pending(等待)
、fulfilled(已完成)
、rejected(已拒绝)
两种状态改变方式: pending => resolved
,pending => rejected
注: 只有异步操作的结果才可以决定当前是哪一种状态,任何其他操作都无法改变这个状态
局限性
- 无法取消 Promise,一旦新建他就会立即执行,中途无法取消;
- 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部;
- 当处于 pending 状态时,无法得知目前进展到哪一阶段(刚刚开始还是即将完成)
用法
先看下面这个例子:
setTimeout( ()=>{
console.log('123');
},0 )
console.log('456');
执行结果相信大家都知道,上面console处于异步代码中(即使延迟为0),下面console处于同步代码中,如果想要 ‘456’ 在 ‘123’ 执行结束后再输出呢?
传统回调函数方式:
setTimeout( ()=>{
console.log('123');
fn();
},0 )
function fn(){
console.log('456')
}
// 123
// 456
使用 Promise 方式:
let promise = new Promise( (resolve,reject) =>{
setTimeout( ()=>{
console.log('123');
resolve('456');
} ,0)
} )
promise.then(function(data){
// resolve状态
console.log(data);
},function(error){
// reject状态
})
// 123
// 456
Promise.prototype.then()
Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数。也就是说,状态由实例化时的参数(两个不同状态的回调函数)执行来决定的,根据不同的状态来执行具体哪个函数。
Promise.prototype.catch()
catch 作为 promise 原型上的方法, 用于执行返回 rejected 状态时传入的回调函数, 可以用于捕获错误。
注: resolve() 和 reject() 的执行会传递到对应的回调函数的 data 或者 error,并且 then 方法返回的是一个新的 Promise 实例,可以继续调用 then 方法, 一般情况下是在 then 方法中执行成功状态时的回调函数,在 catch 方法中执行失败状态时的回调函数
链式操作用法
从表面上看,我们或许会觉得 Promise 只是能够简化传统的层层回调写法。然而,Promise的精髓是 ‘状态’,用维护状态,传递状态的方式来使得回调函数能够及时调用,它比传递 callback 函数要灵活、简单的多。下面为一个 Promise 的使用场景:
async1()
.then(function(data){
console.log(data);
return async2();
})
.then(function(data){
console.log(data);
return async3();
})
.then(function(data){
console.log(data);
});
function async1(){
let p = new Promise(function(resolve,reject){
// 异步操作
setTimeout(()=>{
console.log('异步任务1执行完成!');
resolve('数据1');
},1000);
});
return p;
};
function async2(){
let p = new Promise((resolve,reject)=>{
// 异步操作
setTimeout(()=>{
console.log('异步任务2执行完成!');
resolve('数据2');
},2000);
});
return p;
};
function async3(){
let p = new Promise((resolve,reject)=>{
// 异步操作
setTimeout(()=>{
console.log('异步任务3执行完成!');
resolve('数据3');
},3000);
});
return p;
}
// 1秒后...
// 异步任务1执行完成!
// 数据1
// 2秒后...
// 异步任务2执行完成!
// 数据2
// 3秒后...
// 异步任务3执行完成!
// 数据3
在 then 方法中,可以不用 return Promise实例对象,直接返回数据在后面的 then 中也能够接收到数据
reject用法
在前面的例子中只有 resolve(成功状态)的回调,实际应用中还会有失败状态,reject 就是把 Promise 的状态设置为 rejected ,这样我们就能够在 then 中捕捉到,然后执行相应的回调
let num = 10;
let p1 = function(){
return new Promise((resolve,reject)=>{
if(num <= 5){
resolve("<=5,走resolve");
console.log("resolve不能结束Promise");
}
else{
reject(">5,走reject");
console.log("reject不能结束Promise")
}
})
}
p1()
.then(function(data){
console.log(data)
},function(error){
console.log(error)
})
// reject不能结束Promise
// >5,走reject
resolve 和 reject 永远在当前环境的最后面执行,所以后面的同步代码会先执行
如果 resolve 和 reject 之后还有代码需要执行,最好放在 then 里,然后在 resolve 和 reject 前面写上 return
Promise.prototype.all()
Promise.all() 方法用于将多个 Promise 实例包装成一个新的 Promise 实例
const p = Promise.all( [p1,p2,p3] );
p 的状态由 p1、p2、p3 决定,分为两种情况:
- 只有 p1、p2、p3 的状态都为 resolved 时,p 状态才会变成 resolved,此时 p1、p2、p3 的返回值组成一个数组传递给 p 的回调函数
- 只要 p1、p2、p3之中有一个状态为 rejected ,p 的状态就变成 rejected,此时第一个状态为 rejected 的实例的返回值会传递给 p 的回调函数
由于p 是包含3个 Promise 实例的数组,只有这三个实例状态都为 resolved,或者其中的实例状态为 rejected 时,才会调用 Promise.all 方法后面的回调函数
如果作为参数的 Promise 实例自己定义了 catch 方法,那么它一旦被 rejected,并不会触发 Promise.all 的 catch 方法,如果没有实例参数定义自己的 catch,就会调用 Promise.all 的 catch 方法
Promise.prototype.race()
Promise.race() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例
const p = Promise.race( [p1,p2,p3] )
使用该方法时,只要 p1、p2、p3 之中有一个实例率先改变状态(不管是成功还是失败状态
),p 的状态就会跟着该实例变化,该实例的返回值传递给 p 的回调函数
Promise.resolve()
在实际应用中有时需要将一个对象转为 Promise 对象,Promise.resolve() 方法就能够实现,该实例的状态为 resolved
const promise = Promise.resolve( '123' );
// 等价于
new Promise( resolve => resolve( '123' ) )
Promise.resolve 方法的参数类型有四种情况:
-
参数为一个 Promise 实例
- 如果参数就是 Promise 实例,那么 Promise.resolve 将不做任何修改,依然返回该实例
-
参数为一个 thenable 对象
- thenable 对象指具有 then 方法的对象,如:
let thenable = {
then: function( resolve,reject ){
resolve( 'aaa' );
}
}
- Promise.resolve 方法会将这个对象转为 Promise 对象,然后立即执行 thenable 对象的 then 方法
let thenable = {
then: function(resolve, reject) {
resolve( 'aaa' );
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // aaa
})
上面代码中,thenable 对象的 then 方法执行后,对象 p1 的状态就变为 resolved,从而立即执行后面那个 then 方法指定的回调函数,输出 aaa
-
参数不是具有 then 方法的对象,或者根本不是一个对象
- 如果参数是一个原始值,或者不是一个具有 then 方法的对象,则 Promise.resolve 方法返回一个新的 Promise 对象,状态为 resolved
const p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
上面代码生成一个新的 Promise 对象的实例 p,由于字符串 Hello 不属于异步操作( String 对象不具有 then 方法),返回 Promise 实例的状态生成就为 resolved,所以回调函数会立即执行,且 Promise.resolve 方法的参数会同时传给回调函数
-
参数为空
-
Promise.resolve 方法可以不带参数使用,此时直接返回一个 resolve 状态的 Promise 对象
-
如果希望得到一个 Promise 对象,比较方便的方法就是直接调用不带参数的 Promise.resolve 方法
-
const p = Promise.resolve();
p.then(function () {
// ...
});
- 上面变量 p 就是一个 Promise 对象,注: 立即 resolve 的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
上面代码中,setTimeout(fn,0)在下一轮“事件循环”开始时执行,Promise。resolve() 在本轮执行,console.log('one')则是立即执行,因此最先输出
Promise.reject()
Promise.reject( reason ) 方法也会返回一个新的 Promise 实例,该实例的状态为 rejected
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了
上面代码生成一个 Promise 对象的实例 p,状态为rejected,回调函数会立即执行
注: Promise.reject() 方法的参数,会原封不动的作为 reject 的理由,变成后续方法的参数。这一点与 Promise.resolve 方法不一致
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
上面代码中, Promise.reject 方法的参数为一个 thenable 对象,执行以后,后面 catch 方法的参数不是 reject 抛出的 ‘出错了’ 这个字符串,而是 thenable 对象
Promise.prototype.done
Promise 对象的回调链,不管以 then 方法或者 catch 方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(Promise内部的错误不会冒泡到全局)。因此,可以提供一个 done 方法,总是处于回调链的末端,保证抛出任何可能出现的错误被捕捉
asyncFunc()
.then(f1)
.catch(r1)
.then(f2)
.done();
上面代码可见,done 方法的使用,可以像 then 方法那样用,提供 resolved 和 rejected 状态的回调函数,也可以不提供任何参数。总之,done 都能够捕捉到任何可能出现的错误,并向全局抛出
Promise.prototype.finally
finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。它与 done 方法最大的区别是 finally 方法能够接收一个普通的回调函数作为参数,该函数不管怎样都必须执行
在项目中我们发起请求时,可以使用 finally 方法来对该请求做最终处理,比如关闭对话框等。
asyncFun()
.then(()=>{
// 接收收据
...
})
.catch(()=>{
// 捕获错误
...
})
.finally(()=>{
// 关闭对话框
...
});
上面代码中,不管前面的 Promise 是resolved 状态还是 rejected 状态,都会执行回调函数callback