Promise-基础原理与使用
前言
假设这样一个场景,需要按照顺序发送三个api请求(命名为req1,req2,req3),但是只有在前一个请求发送成功的情况下,后一个请求才能发送(失败即停止)。那么会出现四种情况
- 第一个请求发送失败,结束,什么都没发送
- 第二个请求发送失败,结束,发送了req1
- 第三个请求发送失败,结束,发送了req1,req2
- 四个请求全发送成功,结束,发送了req1,req2,req3
如下代码所示。如果使用jQuery
来实现上述的逻辑,就会出现层层嵌套的情况,这样会导致代码丧失了阅读性。
1 | const url1 = "http://127.0.0.1:5000/persons"; |
如果请求更多,那就需要一直嵌套下去,这就叫做回调地狱(后一个操作需要以前一个操作的状态作为条件)。
为了解决这种问题,Promise
功不可没,如axios
库就使用了Promise
的特性。因此理解Promise
就变得至关重要。
Promise
Promise
是在 ES6(ECMAScript 2015)中引入的重要特性,其设计的主要目标之一就是为了解决“回调地狱”(Callback Hell)的问题。
三种状态
Promise有三种状态:
- pending(进行中)
- fulfilled(成功)
- rejected(失败)
flowchart LR a[Promise\n(只能处于三种状态的任意一种)] -.-> b([pending]) a -.-> c([fulfilled]) a -.-> d([rejected])
在Promise
对象中,有resolve
和reject
两个方法,分别用来表示fulfilled和rejected两种状态。可以使用.then
来获取当前Promise
对象的结果,其中的参数为两个回调函数,第一个回掉处理fulfilled的状态,第二个处理rejected状态。
1 | let p = new Promise((resolve,reject)=>{ |
1 | let p = new Promise((resolve,reject)=>{ |
如果在promise中不使用resolve
和reject
两种方法且无返回值,那么对象就会一直处于pending状态。体现在.then
中即为无操作被执行。
1 | let p = new Promise((resolve,reject)=>{ |
then和catch
.then(value, reason)
可以处理fulfilled和rejected状态.catch(null, reason)
专门处理rejected状态
正确的写法是将.catch
直接衔接在.then
之后。
1 | p.then( |
下面的示例的写法是错误的。
1 | let p = new Promise((resolve,reject)=>{ |
控制台还是会出现Uncaught (in promise)
的报错,这是因为js是从上往下执行的,先执行完.then
函数,发现没有对rejected进行处理,之后才会运行到.catch
的部分,因此系统默认会先抛出Uncaught (in promise)
的错误,然后打印.catch
的执行结果。
then返回值规则
.then
有两种返回值的情况。
- 返回除了
Promise
对象的其他值,例如null、undefined、”abc”、1、NaN等等情况,那么返回的是fulfilled状态 - 返回
Promise
对象,则.then
的返回值和Promise
同步
先证实第一种情况
1 | let p = new Promise((resolve,reject)=>{ |
由于p
调用reject
,因此p2
中进入reason
进行处理,打印失败1
,并返回非promise值”333”,因此p2.then
获得的是fulfilled状态,进入value
处理,打印的是成功2
。
证实第二种情况
1 | let p = new Promise((resolve, reject) => { |
由于p
调用reject
,因此p2
中进入reason
进行处理,打印失败1
,并返回Promise.reject()
,因此p2.then
获得的是rejected状态,进入reason
处理,打印的是失败2
。
修改p.then
返回值为Promise.reject("333")
1 | let p = new Promise((resolve, reject) => { |
分析方法相同,最终打印的是失败1
和成功2
如果将返回改为
new Promise(() => {})
,则返回状态为pedding(promise对象resolve、reject都没调用),则p2.then
无打印。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 let p = new Promise((resolve, reject) => {
reject("111");
});
let p2 = p.then(
(value) => {
console.log("成功1", value);
return "222";
},
(reason) => {
console.log("失败1", reason); // 打印
return new Promise(() => {});
}
);
// 什么都不打印
p2.then(
(value) => {
console.log("成功2", value);
},
(reason) => {
console.log("失败2", reason);
}
);
常用api
Promise
的常用api
Promise.resolve
- 表示返回一个状态为fulfilled的promise对象Promise.reject
- 表示返回一个状态为rejected的promise对象Promise.all([p1,p2,p3])
- 传入一个存储promise对象的列表,只有所有promise都返回fulfilled,则函数的返回值才为fulfilled,否则为rejectedPromise.race([p1,p2,p3])
- 传入一个存储promise对象的列表,看列表中promise对象谁先返回fulfilled,返回值就为该promise对象的返回值
前三个比较常见,不在举例。下面是一个Promise.race
的例子
1 | const p1 = new Promise((resolve, reject) => { |
由于p1
最先返回fulfilled状态,因此返回值为111
axios.all
是使用Promise.all
实现的。
尝试解决
尝试使用axios
来解决回调地狱的问题。
首先要了解到,axios的返回值就是一个promise
对象。
1 | console.log(axios.get(url1)); |
打印的得到的结果如下
可以看到返回值为Promise
对象,请求的数据存储在对象的data
的字段。
根据上文对Promise
原理的了解(或者在上图中,能看到then
方法),我们可以使用axios
改写开头的需求。
1 | const url1 = "http://127.0.0.1:5000/persons"; |
首先,我们使用axios
发送第一个请求,如果请求成功,那么p1.then
走response
回调函数,并同时发送第二个请求,返回请求的axios
对象;如果第二次请求成功,p2.then
走response
回调函数,并同时发送第三个请求;同理,第三次请求成功,p3.then
走response
的回调函数,得到打印数据。
如果任意一个请求发送失败,则会在任意一个error
回调中返回new Promise(() => {})
,返回的是一个处于pedding的Promise对象,那么之后的.then
函数将永远无法走通,也就达到了失败即停止的目的。
将url2
写错,模拟中途出错的情况,控制台得到的结果如下
当axios.get(url2)
请求失败,返回为rejected状态,因此p2.then
走error
回调函数,打印第二次请求失败了
,并将后续的Promise置空,达到错误即停止的逻辑。
上述代码可以去掉中间的变量,使用.then
的链式结构。
1 | axios.get(url1) |
从代码的运行结果来看,完美实现了开头的逻辑。且从代码结构来看,层层缩进的问题被.then
链式调用解决了,即成功解决了回调地狱的问题。
注意:如果在
error
回调函数中返回Promise.reject(error)
,那么如果出现请求错误,会导致一直调用后续的error
回调函数,在逻辑上出现了严重错误(不符合开头出错即停止的业务逻辑。抛开业务逻辑,即便第三个链接是可以访问的,也会打印请求失败)。
async和await
async
和await
是ES7引入的新关键字,用于处理异步操作,两个关键字是成对使用的。
await
会同步等待一个异步操作完成,若未完成则会挂起当前函数。如果未完成,表现在程序运行上为阻塞当前的运行流程。
1 | async function getData() { |
上述代码由于使用await
来等待永远处于pending状态的Promise对象,导致程序运行阻塞,控制台只能打印出1
。因此可以简单将await
理解为只能等到成功的情况。
将上述代码的Promise
对象换为axios
,得到如下代码
1 | async function getData() { |
假设第一个请求不成功,那么打印语句也永远不会执行;如果控制台打印出了第一个请求成功
,那么一定说明第一次请求是成功的。这正好符合一开始说明的业务逻辑。
更进一步,利用async
和await
关键字,可以将上文.then
的链式调用改得更为简洁。
1 | const url1 = "http://127.0.0.1:5000/persons"; |
只要能够运行到await axios.get(url2)
,那么我们一定可以说第一次请求是成功的,因为如果不成功,那么一定会一直等待下去;同理,只要能运行到await axios.get(url3)
,那么第二次请求一定是成功的;同理,只要能打印第三次请求成功
,那么第三次请求也是一定成功的。
利用async
和await
关键字组合,大大提高了代码阅读的可行性,解决了回调地狱的问题,同时还降低了解决问题的门槛(即便使用者完全不了解promise原理,也知道如何轻松解决这个问题)。
假设第二次请求失败,会得到和上文.then
调用如出一则的结果。
事实上即便请求失败程序也不会阻塞,
axios
会默认对404的响应抛出错误(如上图显示)。