Promise基础以及使用示例
什么是Promise
Promise,中文意思是承诺,预示。Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。
Promise基础
期约状态
期约是一个有状态的对象,可能处于以下3种状态
- 待定(pending)
- 兑现(fullfiled,有时候也称为“解决”,resolved)
- 拒绝(rejected)
待定是期约最初始的状态,它可以落定为兑现(fullfiled),也可以落定为拒绝(reject)状态,但是无论落定为哪种状态都是不可逆的。
Promise的简单案例
例子:异步实现延迟执行方法,延迟3秒打印
function delay(ms){
return new Promise(resolve=>{ setTimeout(resolve,ms)})
}
// 直接使用返回promise的方法
delay(3000).then(()=>{
console.log('hahahahah');
})
async/await改写去调用
function delay(ms){
return new Promise(resolve=>{ setTimeout(resolve,ms)})
}
// 利用async/await改写
async function useDelay(ms){
var temple = await delay(ms)
console.log(1111);
return temple
}
// 调用async/await改写后的方法
useDelay(10000)
Promise简单封装axios
封装promise+axios通用请求函数
真实用到的可能会有很多设置,例如header头部会有类似的鉴权设置等,但下面都不考虑,这里实现的是最最简单的功能,主要是方便知道大致的使用。
// promise封装使用,简单实用,至于有其他需求可以自行添加调整
function api(url,params){
return new Promise((resolve,reject)=>{
axios.post(url,params).then((res) => {
// 这里判断具体看项目
if(res.status==200){
resolve(res.data)
}else{
reject(new Error('request [' + url + '] status:' + res.data.message))
}
}).catch(error=>{
reject(new Error('网络错误'))
})
})
}
直接调用上面的封装方法
// 直接简单使用
api('/mock/api/getAppInfo',{a:'111'}).then(res=>{
console.log('res接口返回',res);
})
使用async/await调用上面的封装方法
async function request(url,params){
// 写法1
// await api(url,params).then((res)=>{
// console.log('res-then',res);
// })
// 写法2
let res = await api(url,params)
console.log('res-await',res);
}
request('/mock/api/getAppInfo',{a:'111'})
基础功能(then catch finally)
基本概念
- then():为 Promise 实例添加状态改变时的回调函数,
then
方法的第一个参数是resolved
状态的回调函数,第二个参数是rejected
状态的回调函数,它们都是可选的。 - catch():用于指定发生错误时的回调函数
- finally():不管 Promise 对象最后状态如何,都会执行的操作
链式调用then
- 每次你对 Promise 调用 then(..),它都会创建并返回一个新的 Promise,我们可以将其链接起来;
- 不管从 then(..) 调用的完成回调(第一个参数)返回的值是什么,它都会被自动设置为被链接 Promise(第一点中的)的完成。
链式调用-连续相加(then)
下面有两种调用的方式,其实都是一样的效果,只是在这里这个例子里面可以有两种调用方法,都写出来了。
// 定义一个add的数字
function add(num){
return new Promise((resolve,reject)=>{
resolve(num+1)
})
}
// 方案1,利用add来
add(1).then(res=>{
console.log('展示1',res);
return add(res)
}).then(res=>{
console.log('展示2',res);
return add(res)
}).then(res=>{
console.log('展示3',res);
})
// 控制台按顺序打印
// 展示1 2
// 展示2 3
// 展示3 4
// 方案2,直接返回相加的数
add(1).then((res)=>{
console.log('展示1',res);
return res+1
}).then((res)=>{
console.log('展示2',res);
return res+1
}).then((res)=>{
console.log('展示3',res);
})
// 控制台按顺序打印
// 展示1 2
// 展示2 3
// 展示3 4
链式调用-实际运用(then,catch)
这个 Promise 链不仅是一个表达多步异步序列的流程控制,还是一个从一个步骤到下一个步骤传递消息的消息通道。
下面就是有两个异步的方法,需要按照先后顺序执行,使用链式调用,并且捕获错误
// 实际链式运用
function api(url,params){
return new Promise((resolve,reject)=>{
axios.post(url,params).then((res) => {
// 具体项目去判断
if(res.data.code==200){
resolve(res.data)
}else{
reject(res.data)
}
}).catch(error=>{
reject(new Error('网络错误'))
})
})
}
// 第一种调用方式:直接调用,错误捕获
api('/mock/api/request1').then(res=>{
console.log('第一步请求',res);
return api('/mock/api/request2')
}).then(res=>{
console.log('第二步请求',res);
}).catch(error=>{
console.log('error',error);
}).finally(()=>{
console.log('finally');
})
// 第二种调用方式:async/await调用【推荐使用】
// 异步方法同步执行,多个await任务,用try..catch结构去捕获错误
async function step() {
try {
const res1 = await api('/mock/api/request1');
console.log('async-请求1',res1);
const res2 = await api('/mock/api/request2');
console.log('async-请求2',res2);
} catch (error) {
console.log('error',error);
}
}
step()
由于上述示例是用了两种调用方式,实际使用的时候,选中其一即可。
如果没有报错,都能顺利访问,控制台打印如下:
如果第一个访问错误,catch会捕获,控制台打印如下:
但是上述引申另外一个问题,两个异步请求不存在继发关系,但是向上面这样子的话,如果请求1报错,那么请求2即使没问题也触发不了,阻断了。为此如果两个都是独立的异步操作(不互相依赖),那么可以使用下面Promise.all()方法调用。
总结then()、catch()、finally()
方法 | 调用条件 |
---|---|
Promise.prototype.then() | then 方法的第一个参数是resolved 状态的回调函数,第二个参数是rejected 状态的回调函数,它们都是可选的 |
Promise.prototype.catch() | 用于指定发生错误时的回调函数 |
Promise.prototype.finally() | 用于指定不管 Promise 对象最后状态如何,都会执行的操作。 |
特殊方法(all(),race(),allSettled(),any())
其实在上面四个方法中,个人最常用的是all(),其他都不是很熟悉。所以all相对会比较有实际应用来介绍。
Promise.all()
Promise.all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
- Promise.all()就是两者都会触发到,
promises
是包含N个 Promise 实例的数组,只有这N个实例的状态都变成fulfilled
,或者其中有一个变为rejected
并且完成之后才调用promise.all()方法后的回调函数。另外,Promise.all()
方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。 - 对 Promise.all([ .. ]) 来说,只有传入的所有 promise 都完成,返回 promise 才能完成。 如果有任何 promise 被拒绝,返回的主 promise 就立即会被拒绝(抛弃任何其他 promise 的 结果) 。如果完成的话,你会得到一个数组,其中包含传入的所有 promise 的完成值。对于
下面实例都是Promise.all+.map使用,这里简单介绍一下map()函数
map()
方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。
实例1:如果接口A是需要在接口B和接口C执行完之后才执行,就能用到Promise.all()
// 实际链式运用
function api(url,params){
return new Promise((resolve,reject)=>{
axios.post(url,params).then((res) => {
// 具体项目去判断
if(res.data.code==200){
resolve(res.data)
}else{
reject(res.data)
}
}).catch(error=>{
reject(new Error('网络错误'))
})
})
}
let res1 = api('/mock/api/request1'); // 接口B
let res2 = api('/mock/api/request2'); // 接口C
Promise.all([res1,res2].map((p)=>{
return p.catch(error=>{
// 会打印上述Promise最先返回reject的Promise的报错
console.log('报错',error);
})
})).then(res=>{
// 当Promise.all中的Promise实例的状态都转变了就会走到这里
console.log('res--then',res);
// 用这里就可以访问接口A
}).catch(error=>{
console.log('error',error);
})
实例2:表单校验判断,某个方法要在表单1和表单2都校验成功才能执行(以elementui表单为例)
因为个人遇到项目比较多表单,但是表单内容比较多,所以都是一个模块一个模块弄了component组件,将这些组件放在一个页面,页面中类似有一个提交按钮,这时候就需要校验表单里面的数据,所以遇到情况比较多。下面为了方便,我就直接将两个表单直接放在一个页面,主要是示范如何用Promise.all()
<template>
<div>
<h2>表单1</h2>
<el-form :model="ruleForm1" :rules="rules1" ref="ruleForm1" label-width="100px">
<el-form-item label="活动名称" prop="name">
<el-input v-model="ruleForm1.name"></el-input>
</el-form-item>
<el-form-item label="活动区域" prop="region">
<el-select v-model="ruleForm1.region" placeholder="请选择活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
</el-form>
<h2>表单2</h2>
<el-form :model="ruleForm2" :rules="rules2" ref="ruleForm2" label-width="100px">
<el-form-item label="活动名称" prop="name">
<el-input v-model="ruleForm2.name"></el-input>
</el-form-item>
<el-form-item label="活动区域" prop="region">
<el-select v-model="ruleForm2.region" placeholder="请选择活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
</el-form>
<el-button @click="submit">提交</el-button>
</div>
</template>
<script>
export default {
data(){
return{
ruleForm1:{
name:'',
region:''
},
ruleForm2:{
name:'',
region:''
},
rules1: {
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
],
region: [
{ required: true, message: '请选择活动区域', trigger: 'change' }
],
},
rules2: {
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
],
region: [
{ required: true, message: '请选择活动区域', trigger: 'change' }
],
}
}
},
methods:{
submit(){
const form1 = this.$refs.ruleForm1;
const form2 = this.$refs.ruleForm2;
Promise.all([form1,form2].map(this.getFormPromise)).then(res=>{
console.log('res---',res);
const validateResult = res.every(item => !!item);
if(validateResult){
console.log('表单都校验通过');
}else{
console.log('表单未全部通过');
}
}).catch(e=>{
console.log('error---',e);
})
},
// 表单校验
getFormPromise(form){
return new Promise(resolve=>{
form.validate(res=>{
resolve(res)
})
})
}
}
}
</script>
效果测试过程如图所示
Promise.allSettled()
Promise.all()
方法会在任何一个输入的 Promise 被拒绝时立即拒绝。相比之下,Promise.allSettled()
方法返回的 Promise 会等待所有输入的 Promise 完成,不管其中是否有 Promise 被拒绝。如果你需要获取输入可迭代对象中每个 Promise 的最终结果,则应使用 allSettled()
方法。
具体情况具体使用,下面只是实例解释一下Promise.all()
和Promise.allSettled()
区别
实例:访问两个接口
下面实例的前提:这里,只有request1,request2是存在的接口,request3是不存在的接口
先贴出代码图片以及效果图片解释,最后会贴出代码
let p1 = axios.post('/mock/api/request1');
let p2 = axios.post('/mock/api/request2');
let p3 = axios.post('/mock/api/request3');
Promise.all([p1,p2]).then((res)=>{
// Promise.all([p1,p2,p3]).then((res)=>{
// 全部成功的时候会走到这里
console.log('then-all',res);
}).catch(error=>{
// 一旦有一个Promise中断(rejected),那么就走这里
console.log('error-all',error);
})
Promise.allSettled([p1,p2]).then(res=>{
// Promise.allSettled([p1,p2,p3]).then(res=>{
console.log('then-allSettled',res);
// 可以对每个结果进行处理,比如根据状态判断是否成功
res.forEach((result) => {
if (result.status === "fulfilled") {
// 如果成功,打印数据
console.log('成功',result.value.data);
} else {
// 如果失败,打印失败原因
console.log('失败',result.reason);
}
});
})
Promise.race()
对 Promise.race([ .. ]) 来说,只有第一个决议的 promise(完成或拒绝)取胜,并且其 决议结果成为返回 promise 的决议。
- 一旦有任何一个Promise决议为完成,Promise.race([ .. ]) 就会完成;一旦有任何一个Promise决议为拒绝,它就会拒绝。
当心!若向Promise.all([ .. ])传入空数组,它会立即完成,但Promise. race([ .. ]) 会挂住,且永远不会决议。
let p1 = axios.post('/mock/api/request1');
let p2 = axios.post('/mock/api/request2');
Promise.race([p1,p2]).then(res=>{
console.log('res',res);
}).catch(e=>{
console.log('error',e);
})
Promise.any()
Promise.any()
跟Promise.race()
方法很像,只有一点不同,就是Promise.any()
不会因为某个 Promise 变成rejected
状态而结束,必须等到所有参数 Promise 变成rejected
状态才会结束。
- 只要有一个Promise实例请求成功,调用then回调函数
- 全部Promise实例请求失败,才会调用catch回调函数
验证上面总结的
全部Promise实例请求成功
至少有一个Promise实例请求成功
全部Promise实例请求失败
总结特殊方法比较
all()和allSettled()
方法 | 触发then回调条件 | 触发catch回调条件 |
---|---|---|
Promise.all() | 全部Promise实例成功才调用then函数 | 至少一个Promise实例失败调用catch函数 |
Promise.allSettled() | 只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled 还是rejected ) | —— |
race()和any()
方法 | 触发then回调条件 | 触发catch回调条件 |
---|---|---|
Promise.race() | 一旦有任何一个Promise决议为完成,就会调用then回调 | 一旦有任何一个Promise决议为拒绝,就会调用catch回调 |
Promise.any() | 只要有一个Promise实例请求成功,调用then回调函数 | 全部Promise实例请求失败,才会调用catch回调函数 |
拓展问题
这些问题都是在 前端面试宝典找的,部分感觉能够解惑或者更加了解Promise,所以补充一下,方便对Promise的认识更加了解
1.setTimeout和 Promise 的执行顺序
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function(resolve, rejec
console.log(2)
for (var i = 0; i < 10000; i++) {
if(i === 10) {console.log(10)}
i == 9999 && resolve();
}
console.log(3)
}).then(function() {
console.log(4)
})
console.log(5);
// 输出答案为2 10 3 5 4 1
要先弄清楚settimeout(fun,0)何时执行,promise何时执行,then何时执行
settimeout这种异步操作的回调,只有主线程中没有执行任何同步代码的前提下,才会 执行异步回调,而settimeout(fun,0)表示立刻执行,也就是用来改变任务的执行顺序, 要求浏览器尽可能快的进行回调 promise何时执行,由上图可知promise新建后立即执行,所以promise 构造函数里代码 同步执行的, then方法指向的回调将在当前脚本所有同步任务执行完成后执行, 那么then为什么比settimeout执行的早呢,因为settimeout(fun,0)不是真的立即执行,
经过测试得出结论:执行顺序为:同步执行的代码-》promise.then->settimeout
2.如果已经有三个promise,A、B 和 C,想串行执行,该怎么写?
// promise
A.then(B).then(C).catch(...)
// async/await 异步方法同步执行
(async ()=>{
await a();
await b();
await c();
})()
3.Promise then 第二个参数和catch的区别是什么?
- then 方法的第二个参数和 catch 方法都是用于处理 Promise 的 rejected 状态的情况,但前者需要在每次调用 then 方法时都传递第二个参数,而后者则可以链式调用。
- catch 方法相当于 then 方法的第二个参数,也是用于处理 Promise 的 rejected 状态的情况。不同之处在于,catch 方法可以链式调用,而不需要在每次调用 then 方法时都传递第二个参数。
注意:
- 如果两个都写上(then第二参数和catch回调),那么看是先then后catch还是先catch后then模式【p.then(..).catch(..) 还是 p.catch(..).then(..)】,哪个紧跟着p,就执行哪个,但是catch后面的then依然会继续执行的
上面示例代码如下:
function asyncFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Something went wrong'));
}, 1000);
});
}
// 示例1,2效果是一样的
// 示例1
asyncFunction()
.then(result => console.log(result),reject=>{
console.error('then-第二个参数',reject); // then-第二个参数 Error: Something went wrong
})
// 示例2
asyncFunction()
.then(result => console.log(result))
.catch(error => console.error('catch回调',error)); // catch回调 Error: Something went wrong
// 示例3 两个都用,先then后catch
asyncFunction()
.then(result => console.log('先then后catch',result),reject=>{
console.error('先then后catch-then的reject',reject); // 先then后catch-then的reject Error: Something went wrong
}).catch(error => console.error('先then后catch-catch回调',error)); // 无
// 示例4 两个都用,先catch后then
asyncFunction().catch(error => console.error('先catch后then-catch回调',error))// 先catch后then-catch回调 Error: Something went wrong
.then(result => {
console.log('catch后then',result) // catch后then undefined
},reject=>{console.error('先catch后then-then的reject',reject); // 无
4.如何让Promise.all在抛出异常后依然有效
该方法指当所有在可迭代参数中的 promises
已完成,或者第一个传递的 promise(指 reject)失败时,返回 promise。但是当其中任何一个被拒绝的话。Promise.all([..])
就会立即被拒绝,并丢弃来自其他所有promis的全部结果。
也就是说,promise.all
中任何一个 promise
出现错误的时候都会执行reject,导致其它正常返回的数据也无法使用。
方案1:在promise.all队列中,使用map每一个过滤每一个promise任务,其中任意一个报错后,return一个返回值,确保promise能正常执行走到.then中。
var p1 = new Promise((resolve, reject) => {
resolve('p1');
});
var p2 = new Promise((resolve, reject) => {
resolve('p2');
});
var p3 = new Promise((resolve, reject) => {
reject('p3');
});
Promise.all([p1, p2, p3].map(p => p.catch(e => '出错后返回的值' )))
.then(values => {
console.log(values);
}).catch(err => {
console.log(err);
})
PS:这就是为什么我常用all()而没有用allSettled(),但也能很好实现类似allSettled()的效果,上面Promise.all()的例子我都是有使用方案1的方法【主要用习惯了】
方案2:用Promise.allSettled
替代 Promise.all()
Promise.allSettled()
方法返回一个promise,该promise在所有给定的promise已被解析或被拒绝后解析,并且每个对象都描述每个promise的结果。
5.使用Promise实现红绿灯交替重复亮
红灯3秒亮一次,黄灯2秒亮一次,绿灯1秒亮一次;如何让三个灯不断交替重复亮灯?
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
function light(time,call){
return new Promise(resolve=>{
setTimeout(()=>{
call()
resolve()
},time)
})
}
function step(){
Promise.resolve().then(()=>{
return light('3000',red)
}).then(()=>{
return light('2000',yellow)
}).then(()=>{
return light('1000',green)
}).then(()=>{
return step()
})
}
step()
小结:虽然做了前端好几年了,但是对于基础的东西或者比较少用的都是用到查找想用的使用(平时最多就是用Promise封装axios公共请求或者有时候用到Promise.all()),而且对其他功能很多都没有应用场景去使用,就更加难记得,,每次准备面试都要复习一遍,这次打算做个Promise的小总结,方便以后供翻阅查询。【因为有些案例是自己写的,有记忆会和实用场景容易理解】
参考:
《你不知道的JavaScript中卷》
前端面试题宝典中的Promise题目
转载自:https://juejin.cn/post/7256594892721684541