大家好,我是辉夜真是太可爱啦。这是我写的一个一文搞懂JS系列专题。文章清晰易懂,会将会将关联的只是串联在一起,形成自己独立的知识脉络,整个合集读完相信你也一定会有所收获。写作不易,希望您能给我点个赞!
合集地址:一文搞懂JS系列专题
概览
-
食用时间: 15-20分钟
-
难度: 简单,别跑,看完再走
-
食用价值: 循序渐进了解Promise的概念,使用方法以及特性,还有Promise的痛点,以及最新的Promise.any()以及Promise.allSettled()。
-
铺垫知识:
① 如果关于同步任务、异步任务不懂的同学可以移步看下我的这一篇博客,一文搞懂JS系列(六)之微任务与宏任务,Event Loop,
② 如果关于实例对象,原型,原型链不懂的同学可以参考下我的这篇文章 一文搞懂JS系列(七)之构造函数,new,实例对象,原型,原型链,ES6中的类
使用环境
Promise 本质上是一个构造函数,因为使用 new 关键字创建,它是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大,属于 ES6 中的一个概念。
我们都知道,代码是自上而下逐行执行的,但是,有些任务,它是异步的,所以相对同步任务,会滞后执行,当我们需要后面的同步代码都等到这个异步任务执行完成之后再执行,那么,就需要用到 Promise 了。
简单来说,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=>{
})
}
})
当然,随着项目的复杂度,有时候可能需要四层五层,虽然这种情况应该比较少,而这其中又夹杂着闭包的概念,内部可以访问外层的结果,一层又一层的接口返回数据,维护的时候头都看晕了。
修改的时候还要先看到底是改第几层的代码,以防止改错地方。
链式调用
其实, Promise 本来就是用来解决回调地狱所带来的问题的,所以,其实可以用链式调用的方式,来解决层层嵌套所带来的问题。
它的原理主要是 .then() 回调返回的也是一个 Promise ,并且是一个全新的 Promise,如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装。
所以才可以一直 .then() ,代码如下:
getData1().then(res=>{
console.log(res) // data1
return getData2();
}).then(res=>{
console.log(res) // data2
return getData3();
}).then(res=>{
console.log(res) // data3
})
链式回调的写法就比嵌套的写法来的清晰多了,在写法上更像是写同步代码。
当然,下一篇博客会来讲述下 Generator ,以及最终最优雅地方式 async await。
最后,欢迎大家关注我的个人公众号 前端大食堂