一文搞懂JS系列(八)之轻松搞懂Promise
写在最前面:这是我写的一个一文搞懂JS系列专题。文章清晰易懂,会将会将关联的只是串联在一起,形成自己独立的知识脉络,整个合集读完相信你也一定会有所收获。写作不易,希望您能给我点个赞!
合集地址:一文搞懂JS系列专题
概览
-
食用时间: 15-20分钟
-
难度: 简单,别跑,看完再走
-
食用价值: 循序渐进了解Promise的概念,使用方法以及特性,还有Promise的痛点,以及最新的Promise.any()以及Promise.allSettled()。
-
铺垫知识:
① 如果关于同步任务、异步任务不懂的同学可以移步看下我的这一篇博客,一文搞懂JS系列(六)之微任务与宏任务,Event Loop,
② 如果关于实例对象,原型,原型链不懂的同学可以参考下我的这篇文章 一文搞懂JS系列(七)之构造函数,new,实例对象,原型,原型链,ES6中的类
使用环境
Promise
本质上是一个构造函数,因为使用 new
关键字创建,它是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大,属于 ES6
中的一个概念。
简单来说,Promise
其实就是一个异步操作的容器,可以获取用来优雅地获取异步操作的结果。当然你也可以放同步操作,可以,但是不建议。毕竟同步任务一般直接正常书写即可,没必要套个 Promise
的壳子。
所以, Promise
一般用于 $ajax
数据请求,即用于封装 http 请求,例如下面的方式(用的 axios
做的数据请求)
// 向后台发送数据
export const postData= (params) => {
return new Promise((resolve, reject) => {
axios.post('/xxxUrl', params).then(res=>{
resolve(res)})
.catch(error=>{reject(error)});
})
};
三种状态
Promise 实例有三种状态:
-
pending
进行中,这是 Promise 实例创建后的一个初始态。
-
fulfilled
已成功。在执行器中调用
resolve
后,达成的状态。 -
rejected
已失败。在执行器中调用
reject
后,达成的状态。
这当然也很好理解,就和你做事情一样,一开始是进行中,最后,只有成功或者失败。
就拿上面的例子来说, postData 就是一个 Promise
的实例对象,new Promise
之后,它的状态就是 pending ,只有当 axios.post
即网络请求成功或者失败了以后,它的状态才会变更,变更为 fulfilled 或 rejected ,而这个第二种状态,取决于网络请求的结果。
Promise.prototype
-
Promise.prototype.then()
then 方法简单理解就是 resolve() 回调之后的产物,即 已成功 状态下的回调函数。
-
Promise.prototype.catch()
catch 方法简单理解就是 reject() 回调之后的产物,即 已失败 状态下的回调函数。
-
Promise.prototype.finally()
finally 方法简单理解就是 无论成功或失败 ,都会执行的回调函数。
特性
1. 立即执行
在 Promise
实例创建后,执行器里的逻辑会立刻执行,在执行的过程中,根据异步返回的结果,决定如何使用 resolve
或 reject
来改变 Promise 实例的状态。如何理解下面的这句话,先来看一个例子
const promise = new Promise(function(resolve, reject) {
console.log('start');
if (true){
resolve('success');
} else {
reject('error');
}
console.log('end');
});
promise.then(res=>{
console.log(res);
})
根据上面的学习,我们可以知道,在一开始控制台便会输出 start ,毕竟 Promise
在创建以后便会立即执行,然后输出 end ,等到处理完所有同步任务以后,再进行处理异步任务,因为走的是 resolve()
,所以最后输出 success,结果如下
start => end => success
2. 承诺
它另外也有自己一个很大的特点,那就是不受外界的影响。
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
3. 结果唯一性
成功会触发 then
,而失败会触发 catch
,状态不一致则会获取不到结果。可以看下下面的例子
const promise = new Promise(function(resolve, reject) {
console.log('start');
if(true){
resolve('success');
}else{
reject('error');
}
console.log('end');
});
promise.catch(error=>{
console.log(error);
})
由于 Promise
中是 resolve
回调,即成功状态,所以,只会走 then()
而不会触发 catch
。
所以,程序的运行结果为 start => end
, catch()
根本不会执行。
当然,不仅如此, Promise
一旦状态改变,就不会再变。我们来改写下上面的代码,为了加以区分,我们先使用 reject()
然后再调用 resolve()
const promise = new Promise(function(resolve, reject) {
console.log('start');
reject('error');
resolve('success');
console.log('end');
});
promise.then(res=>{
console.log(res);
}).catch(error=>{
console.log(error);
})
由于状态一但改变,就不会再变,所以,它的状态有且只有且一直是初次改变的结果,而首次执行 reject
,即失败状态。
所以,上面的输出结果为 start => end => error
Promise.all()
Promise.all()
方法用于将多个 Promise
实例,包装成一个新的 Promise
实例
例如定义了三个 postData
的实例方法,代码如下
const dataList = Promise.all([postData1(),postData2(),postData3()])
只有所有请求的状态都成功 ,dataList的状态才会变成 fulfilled ,此时 postData1, postData2, postData3的返回值组成一个数组,传递给dataList的回调函数。
只要有一个请求失败,那么,dataList的状态就会变成 rejected ,此时第一个被reject的实例的返回值,会传递给dataList的回调函数。
使用场景:
举个例子,三个接口分别是拉取语文,数学,英语三科成绩的接口,那么,我们需要通过这三个接口的返回,计算学生最后成绩的总分,此时用 all()
就很合理,因为三个接口的值缺一不可,如果有一个发生错误,就得不到总分,就会走 catch()
,然后,提示是哪一科的成绩数据得不到,影响了最终的总分计算。
Promise.race()
Promise.race()
方法同样是将多个 Promise
实例,包装成一个新的 Promise
实例。
const dataList = Promise.rece([postData1(),postData2(),postData3()])
上面代码中,三个 Promise
实例,只要谁率先改变状态,那么,dataList 的状态也就跟着改变,相当于就是谁快,我就用谁
使用场景:
① 当一个接口有三个请求接口地址,请求的数据是一致的时候,为了保证接口的最快速度匹配,可以使用这个方法。
② 在Promise实例中,放入一个延时器函数, setTimeout(() => reject(new Error('request timeout')), 5000)
,可以通过它来设置这个接口的,相当于 postData1() 必须在5秒内完成,否则会直接失败
const dataList = Promise.race([
postData1(),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
Promise.allSettled()
Promise.allSettled()
方法同样是将多个 Promise
实例,包装成一个新的 Promise
实例。(ES 2020 特性)
只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,最终包装成一个数组返回。并且它无论参数实例是成功或失败,始终只会走 .then()
,没有 .catch()
,成功之后返回的结果如下所示:
[
{ status: 'fulfilled', value: 42 },
{ status: 'rejected', reason: -1 }
]
每个对象都有 status
属性,该属性的值只可能是字符串 fulfilled
或字符串 rejected
。 fulfilled
时,对象有 value
属性, rejected
时有 reason
属性,对应两种状态的返回值。
所以,可以通过 status
方法进行区分参数实例的成功或失败。
success = results.filter(p => p.status === 'fulfilled')
error = results.filter(p => p.status === 'rejected')
使用场景:
并不关心接口的结果,只关心这些操作有没有结束。
Promise.any()
Promise.any()
方法同样是将多个 Promise
实例,包装成一个新的 Promise
实例。(ES 2021 特性)
只要参数实例有一个成功,包装实例就会变成成功状态;只有当所有参数实例都失败,包装实例才会变成失败状态。
使用场景:
未想出,待补充
回调地狱
Promise
的功能确实很强大,但有的时候,我们的接口是要按顺序执行,比方说我们要先拉取第一个接口,用第一个接口的参数去拉取第二个接口,然后再去拉第三个接口,此时,必须按顺序执行
getData1('').then(res1=>{
getData2(res1.data.id).then(res2)=>{
getData3(res2.data.id).then(res3=>{
})
}
})
当然,随着项目的复杂度,有时候可能需要四层五层,虽然这种情况应该比较少,这也就是所谓的回调地狱,其中又夹杂着闭包的概念,内部可以访问外层的结果,一层又一层的接口返回数据,维护的时候头都看晕了。
修改的时候还要先看到底是改第几层的代码,以防止改错地方。
当然,下一篇博客会来讲述下 Generator
,以及最终最优雅地方式 async await
。
最后,欢迎大家关注我的个人公众号 前端大食堂