你写过多少个嵌套函数

前言

记得以前写jquery的ajax时,获取到信息后,将信息利用起来,然后再次根据这个信息来发送ajax或者做一些异步回调方法,那代码场景简直不可想象,一层有一层的回调,如下代码,就好比一层层if else的面条代码一样,不过好在ES6将promise标准化,这样就能改善这种回调地狱的问题,至少提高了代码的可读性和降低后期维护成本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$.ajax({  
url: url1,
success: function(data){
$.ajax({
url: url2,
data: data,
success: function(data){
$.ajax({
//...
});
}
});
}
});

什么是Promise?

其实它只是个构造函数,用来传递异步操作信息,链式调用,避免层层嵌套的回调函数,接收两个函数参数,resolve和reject,分别表示异步操作执行成功后的回调和失败的回调,并且一定要注意,Promise在声明的时候就已经执行了。

三种状态:

  • Pending 进行中
  • Fulfilled(resolve) 已完成
  • Reject 已失败

两种过程:

  • pending -> resolved 表示函数执行成功
  • pending -> rejected 表示函数执行失败

注意: 一旦promise状态发生以上任意两种变化,就不能再改变了,任何时候都只能得到改变后的结果。

Promise新建后立马执行

我们来看这么一段代码:

1
2
3
4
5
let promise = new Promise(function(resolve,reject) {
console.log('nihao')
resolve();
})
// 执行这个脚本即输出nihao

我们发现,只是定义了这个对象而已,并没有调用任何方法,就立马执行了,但是我想控制他的执行步骤,那我们该怎么解决这个问题呢?

Promise的真确打开方式

比如我想实现三秒钟后输出你好,并且自己掌握这个函数的调用,如下:

1
2
3
4
5
6
7
8
9
10
11
12
function sayHi(ms){
let promise = new Promise(function(resolve,reject) {
setTimeout(()=>{
resolve('nihao');
},ms)
})
return promise
}

sayHi(3000).then(value => {
console.log(value)
}) // 三秒后输出 nihao

我们可以将promise包裹在一个可执行函数当中,只有执行了这个函数,promise中的业务逻辑才能执行,但是这个then又是什么东西,为什么是通过resolve来传递参数,这里我们一一来解答。

为什么要这样设计Promise

回想一下,promise给我们做了哪些事?简单来说,就是它替你执行了异步函数(如定时器、ajax请求),等到这个异步函数执行完成了,promise就会来通知你:”你要的结果来啦”,这个时候,如果是真确的结果,那么就是通过resolve的参数传给你,然后你通过then来获取这个结果,错误的结果则是通过reject传给你。这个时候就在catch中获得错误信息,下面我用一段ajax请求来细说:

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
function sendAjax(url) {
let promise = new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open("Get", url);
xhr.onreadystatechange = hander; // 状态发生改变时的回调
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
xhr.send();
// 回调函数
function hander() {
if (this.readyState !== 4 ) {
return ;
}
if (this.status == 200) {
resolve(this.response); //请求执行成功,通过resolve参数来返回数据
} else {
reject(new Error(this.statusText)) //请求执行失败,通过reject参数来返回错误信息
}
}
})
return promise // 返回promise实例
}

sendAjax("/products").then(value => {
console.log(value) //value指请求返回数据
}).catch(error => {
console.log(error) // error指抛出错误信息
})

理解这段代码就能解决现实中百分之六十的问题,其实如果你用过axios就知道,axios的用法和上面这段ajax的封装的函数类似,几乎是一样的。当然,剩下的百分之四十的问题,我们通过下面的讲解来解决。

Promise.all

假如我们有这么一个需求,一个列表中的信息需要分别调用两个网络数据,只有这两个请求数据都返回了,我们才能将数据渲染到列表上,这里我们要并发发送请求保证性能,但是却又不知道哪个请求是最后到达,假如没有promise,你可能需要用一个计数器,多次判断计数器数量,如下(伪代码):

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
var allData = [];
function check() {
if (allData.length === 2) {
return true;
} else {
return false;
}
}
$.ajax({
url: "xxx.com/product",
success: function(data) {
allData.push(data);
if (check()) {
// something
}
}
});

$.ajax({
url: "xxx.com/product",
success: function(data) {
allData.push(data);
if (check()) {
// something
}
}
});

这种方法也即是相当于变相的回调地狱,那么我们用promise的all方法就能轻松解决:

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
function getData1(){
let promise = new Promise((resolve, reject) => {
$.ajax({
url: "xxx.com/product",
success: function(data) {
resolve(data)
}
});
})
return promise
}
function getData2(){
let promise = new Promise((resolve, reject) => {
$.ajax({
url: "xxx.com/product",
success: function(data) {
resolve(data)
}
});
})
return promise
}

Promise.all([getData1(),getData2()]).then(data => {
console.log(data) // 输出[data1,data2]
}).catch(e=>{console.log(e)})

这里我们将两个ajax各自封装到promise对象中,然后集中在全局对象中的Promise.all汇合执行,这时候,当两个数据都返回(即触发resolve)后,才会输出数据,注意这个数据是一个数组,分前后位置关系。

但是要注意一点,如果all中的其中一个promise返回了reject,那么Promise.all就只会触发catch.

Promise.race

race就表示比赛,顾名思义表示传入多个promise,首先触发resolve的就会触发Promise.race的resolve,这么说有点绕口,我们来个demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function setTime1(){
let promise = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('setTime1')
},3000)
})
return promise
}

function setTime2(){
let promise = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('setTime2')
},2000)
})
return promise
}

Promise.race([setTime1(),setTime2()]).then(data=>{
console.log(data) // 2秒后输出setTime2
})

这好像看上去,race没什么用,确实,在现实开发中,似乎用到的不多,但是我们可以打开脑洞,做一个网络超市处理:

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
function getData(){
let promise = new Promise((resolve, reject) => {
$.ajax({
url: "xxx.com/product",
success: function(data) {
resolve(data)
}
});
})
return promise
}

function timeOut(){
let promise = new Promise((resolve,reject)=>{
setTimeout(()=>{
reject(new Error('ajax timeOut'))
},2000)
})
return promise
}

Promise.race([getData(),timeOut()]).then(data=>{
console.log(data)
}).catch(e=>{
console.log(e) // 若网络请求在2000ms后没有成,那么就会触发catch
})

Promise链式调用

我们可以在.then中继续返回一个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
var p = new Promise(function(resolve, reject){
resolve(1);
});
p.then(function(value){ //第一个then
console.log(value);
return value*2;
}).then(function(value){ //第二个then
console.log(value);
}).then(function(value){ //第三个then
console.log(value);
return Promise.resolve('resolve');
}).then(function(value){ //第四个then
console.log(value);
return Promise.reject('reject');
}).then(function(value){ //第五个then
console.log('resolve: '+ value);
}, function(err){
console.log('reject: ' + err);
})
/**
* 输出
1
2
undefined
"resolve"
"reject: reject"
*/

总结

Promise给我们开发带来了很多便利,尤其是在逻辑较为复杂的时候,多次嵌套回调也变得较为优雅,大大提升了代码的可读性,也算是为维护你代码的小伙伴谋福利啦,文中只是简单的提了平时开发中用的比较多的几个方法,还有一些较为少用的并没有举例出来,具体可以参照阮老师的ES6入门。