前言

假设这样一个场景,需要按照顺序发送三个api请求(命名为req1,req2,req3),但是只有在前一个请求发送成功的情况下,后一个请求才能发送(失败即停止)。那么会出现四种情况

  • 第一个请求发送失败,结束,什么都没发送
  • 第二个请求发送失败,结束,发送了req1
  • 第三个请求发送失败,结束,发送了req1,req2
  • 四个请求全发送成功,结束,发送了req1,req2,req3

如下代码所示。如果使用jQuery来实现上述的逻辑,就会出现层层嵌套的情况,这样会导致代码丧失了阅读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const url1 = "http://127.0.0.1:5000/persons";
const url2 = "http://127.0.0.1:5000/persons2";
const url3 = "http://127.0.0.1:5000/persons3";

function getData() {
$.ajax(url1, {
success(data) {
console.log(data);
$.ajax(url2, {
success(data) {
console.log(data);
$.ajax(url3, {
success(data) {
console.log(data);
},
error(err) {
console.log(err);
},
});
},
error(err) {
console.log(err);
},
});
},
error(err) {
console.log(err);
},
});
}

如果请求更多,那就需要一直嵌套下去,这就叫做回调地狱(后一个操作需要以前一个操作的状态作为条件)。

为了解决这种问题,Promise功不可没,如axios库就使用了Promise的特性。因此理解Promise就变得至关重要。

Promise

Promise是在 ES6(ECMAScript 2015)中引入的重要特性,其设计的主要目标之一就是为了解决“回调地狱”(Callback Hell)的问题。

三种状态

Promise有三种状态:

  • pending(进行中)
  • fulfilled(成功)
  • rejected(失败)

Promise对象中,有resolvereject两个方法,分别用来表示fulfilled和rejected两种状态。可以使用.then来获取当前Promise对象的结果,其中的参数为两个回调函数,第一个回掉处理fulfilled的状态,第二个处理rejected状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let p = new Promise((resolve,reject)=>{
// 将promise置为fulfilled
resolve();
// reject();
});

p.then(
value => {
console.log('成功',value); // 被打印
},
reason => {
console.log('失败',reason);
}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let p = new Promise((resolve,reject)=>{
// resolve();
// 将promise置为rejected
reject();
});

p.then(
value => {
console.log('成功',value);
},
reason => {
console.log('失败',reason); // 被打印
}
)

如果在promise中不使用resolvereject两种方法且无返回值,那么对象就会一直处于pending状态。体现在.then中即为无操作被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let p = new Promise((resolve,reject)=>{
// resolve();
// reject();
});

// 无任何打印
p.then(
value => {
console.log('成功',value);
},
reason => {
console.log('失败',reason);
}
)

then和catch

  • .then(value, reason)可以处理fulfilled和rejected状态

  • .catch(null, reason)专门处理rejected状态

正确的写法是将.catch直接衔接在.then之后。

1
2
3
4
5
6
7
8
9
p.then(
value => {
console.log('成功',value);
}
).catch(
reason => {
console.log('失败',reason); // 打印失败 undefined
}
)

下面的示例的写法是错误的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let p = new Promise((resolve,reject)=>{
// resolve();
reject();
});


p.then(
value => {
console.log('成功',value);
}
)

p.catch(
reason => {
console.log('失败',reason); // 打印失败 undefined
}
)

控制台还是会出现Uncaught (in promise)的报错,这是因为js是从上往下执行的,先执行完.then函数,发现没有对rejected进行处理,之后才会运行到.catch的部分,因此系统默认会先抛出Uncaught (in promise)的错误,然后打印.catch的执行结果。

then返回值规则

.then有两种返回值的情况。

  • 返回除了Promise对象的其他值,例如null、undefined、”abc”、1、NaN等等情况,那么返回的是fulfilled状态
  • 返回Promise对象,则.then的返回值和Promise同步

先证实第一种情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 "333"
}
)

p2.then(
value => {
console.log('成功2',value); // 打印
},
reason => {
console.log('失败2',reason);
}
)

由于p调用reject,因此p2中进入reason进行处理,打印失败1,并返回非promise值”333”,因此p2.then获得的是fulfilled状态,进入value处理,打印的是成功2

证实第二种情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 Promise.reject("333");
}
);

p2.then(
(value) => {
console.log("成功2", value);
},
(reason) => {
console.log("失败2", reason); // 打印
}
);

由于p调用reject,因此p2中进入reason进行处理,打印失败1,并返回Promise.reject(),因此p2.then获得的是rejected状态,进入reason处理,打印的是失败2

修改p.then返回值为Promise.reject("333")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 Promise.resolve("333");
}
);

p2.then(
(value) => {
console.log("成功2", value); // 打印
},
(reason) => {
console.log("失败2", reason);
}
);

分析方法相同,最终打印的是失败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,否则为rejected
  • Promise.race([p1,p2,p3]) - 传入一个存储promise对象的列表,看列表中promise对象谁先返回fulfilled,返回值就为该promise对象的返回值

前三个比较常见,不在举例。下面是一个Promise.race的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const p1 = new Promise((resolve, reject) => {
resolve("111");
});

const p2 = new Promise((resolve, reject) => {
reject("222");
});

const p3 = new Promise((resolve, reject) => {
resolve("333");
});

Promise.race([p1, p2, p3])
.then((res) => {
console.log(res); // 111
})
.catch((err) => {
console.log(err);
});

由于p1最先返回fulfilled状态,因此返回值为111

axios.all是使用Promise.all实现的。

尝试解决

尝试使用axios来解决回调地狱的问题。

首先要了解到,axios的返回值就是一个promise对象。

1
console.log(axios.get(url1));

打印的得到的结果如下

image-20240830225616533

可以看到返回值为Promise对象,请求的数据存储在对象的data的字段。

根据上文对Promise原理的了解(或者在上图中,能看到then方法),我们可以使用axios改写开头的需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const url1 = "http://127.0.0.1:5000/persons";
const url2 = "http://127.0.0.1:5000/persons2";
const url3 = "http://127.0.0.1:5000/persons3";

const p1 = axios.get(url1);

const p2 = p1.then(
(response) => {
console.log("第一次请求成功了", response.data);
return axios.get(url2);
},
(error) => {
console.log("第一次请求失败了", error);
return new Promise(() => {});
}
);

const p3 = p2.then(
(response) => {
console.log("第二次请求成功了", response.data);
return axios.get(url3);
},
(error) => {
console.log("第二次请求失败了", error);
return new Promise(() => {});
}
);

p3.then(
(response) => {
console.log("第三次请求成功了", response.data);
},
(error) => {
console.log("第三次请求失败了", error);
return new Promise(() => {});
}
);

首先,我们使用axios发送第一个请求,如果请求成功,那么p1.thenresponse回调函数,并同时发送第二个请求,返回请求的axios对象;如果第二次请求成功,p2.thenresponse回调函数,并同时发送第三个请求;同理,第三次请求成功,p3.thenresponse的回调函数,得到打印数据。

如果任意一个请求发送失败,则会在任意一个error回调中返回new Promise(() => {}),返回的是一个处于pedding的Promise对象,那么之后的.then函数将永远无法走通,也就达到了失败即停止的目的。

url2写错,模拟中途出错的情况,控制台得到的结果如下

image-20240830231012633

axios.get(url2)请求失败,返回为rejected状态,因此p2.thenerror回调函数,打印第二次请求失败了,并将后续的Promise置空,达到错误即停止的逻辑。

上述代码可以去掉中间的变量,使用.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
25
26
27
axios.get(url1)
.then(
(response) => {
console.log("第一次请求成功了", response.data);
return axios.get(url2);
},
(error) => {
console.log("第一次请求失败了", error);
return new Promise(() => {});

})
.then((response) => {
console.log("第二次请求成功了", response.data);
return axios.get(url3);
},
(error) => {
console.log("第二次请求失败了", error);
return new Promise(() => {});

})
.then((response) => {
console.log("第三次请求成功了", response.data);
},
(error) => {
console.log("第三次请求失败了", error);
return new Promise(() => {});
});

从代码的运行结果来看,完美实现了开头的逻辑。且从代码结构来看,层层缩进的问题被.then链式调用解决了,即成功解决了回调地狱的问题。

注意:如果在error回调函数中返回Promise.reject(error),那么如果出现请求错误,会导致一直调用后续的error回调函数,在逻辑上出现了严重错误(不符合开头出错即停止的业务逻辑。抛开业务逻辑,即便第三个链接是可以访问的,也会打印请求失败)。

image-20240830232045862

async和await

asyncawait是ES7引入的新关键字,用于处理异步操作,两个关键字是成对使用的。

await会同步等待一个异步操作完成,若未完成则会挂起当前函数。如果未完成,表现在程序运行上为阻塞当前的运行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
async function getData() {
try {
console.log(1); // 打印
const p = new Promise(()=>{})
let p1 = await p; // 阻塞在此
console.log(2);
console.log(3);
console.log(4);

} catch (error) {
console.log("请求失败", error);
}
}

上述代码由于使用await来等待永远处于pending状态的Promise对象,导致程序运行阻塞,控制台只能打印出1因此可以简单将await理解为只能等到成功的情况。

将上述代码的Promise对象换为axios,得到如下代码

1
2
3
4
5
6
7
8
9
async function getData() {
try {
let p1 = axios.get(url1);
console.log("第一个请求成功", p1);

} catch (error) {
console.log("请求失败", error);
}
}

假设第一个请求不成功,那么打印语句也永远不会执行;如果控制台打印出了第一个请求成功,那么一定说明第一次请求是成功的。这正好符合一开始说明的业务逻辑。

更进一步,利用asyncawait关键字,可以将上文.then的链式调用改得更为简洁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const url1 = "http://127.0.0.1:5000/persons";
const url2 = "http://127.0.0.1:5000/persons22";
const url3 = "http://127.0.0.1:5000/persons3";
async function getData() {
try {
let p1 = await axios.get(url1);
console.log("第一次请求成功", p1.data);
let p2 = await axios.get(url2);
console.log("第二次请求成功", p2.data);
let p3 = await axios.get(url3);
console.log("第三次请求成功", p3.data);
} catch (error) {
console.log("请求失败", error);
}
}

只要能够运行到await axios.get(url2),那么我们一定可以说第一次请求是成功的,因为如果不成功,那么一定会一直等待下去;同理,只要能运行到await axios.get(url3),那么第二次请求一定是成功的;同理,只要能打印第三次请求成功,那么第三次请求也是一定成功的。

利用asyncawait关键字组合,大大提高了代码阅读的可行性,解决了回调地狱的问题,同时还降低了解决问题的门槛(即便使用者完全不了解promise原理,也知道如何轻松解决这个问题)。

假设第二次请求失败,会得到和上文.then调用如出一则的结果。

image-20240831000137953

事实上即便请求失败程序也不会阻塞,axios会默认对404的响应抛出错误(如上图显示)。