解决回调地狱

前言

文中将简单整理一下js异步编程的方法。关于js异步编程,早期很容易出现回调地狱这一现象,也出现了一些相应的解决办法,从PromiseGenerator再到async/await,解决了回调地狱这一问题,当然本文只是浅显的举出一些实例,以及个人简单的理解。

异步编程这一块确实还有很多可以继续深入的地方。

异步编程方法

  • 回调函数
  • 事件监听
  • Promise对象
  • Generator函数
  • Generator的语法糖async/await

文中将着重放在PromiseGenerator函数async/await这三个方面进行举例

回调地狱产生

这里首先使用node随便写三个接口测试,具体代码就不贴了,两个Get请求,一个Post请求。

1
2
3
4
5
6
7
8
9
$.get(url + "/first", function(resFirst) {
console.log(resFirst);
$.get(url + "/second", function(resSecond) {
console.log(resSecond);
$.post(url + "/login", userData, function(res) {
console.log(res);
})
})
})

可以看到上面代码就是一个简单的回调地狱,每一个请求都是外层请求的回调。那么有什么问题呢?最明显的一点就是,代码可读性差,就像洋葱一样一层包裹着一层,改动一处,其余地方也要改动,可维护性也不好。

当然回调地狱不止是这么一个问题,还有一个问题就是异常处理上的问题,即在回调中出现的异常无法被捕获,举个例子

1
2
3
4
5
6
7
8
9
10
11
function throwError(){
// console.log("throw");
throw new Error('ERROR');
}

try{
setTimeout(throwError, 3000);
// throwError();
} catch(e){
alert(e);//这里的异常无法捕获
}

上面代码运行后并不会弹出窗口,也就是无法被捕获到,那么类比到最开始举的三个请求的例子,我们不能直接在三个请求整体的外面写try/catch,因为这样无法捕获,而需要在每个回调内部写才能捕获到,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$.get(url + "/first", function(resFirst) {
console.log(resFirst);
try {
throwError();
} catch(e) {
alert(e);
}
$.get(url + "/second", function(resSecond) {
console.log(resSecond);
try {
throwError();
} catch(e) {
alert(e);
}
$.post(url + "/login", userData, function(res) {
console.log(res);
try {
throwError();
} catch(e) {
alert(e);
}
})
})
})

这样代码的弊端非常明显,代码量不仅大,而且异常非常不方便处理,那么下面就将使用Promise改写该段代码,解决上述问题。

Promise改写

Promise是一种异步编程解决方案,有三种状态pendingfulfilledrejected,使用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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 封装的请求
const reqJSON = function(url, data) {
const promise = new Promise(function(resolve, reject){
// 箭头函数不会创建this,而是向上找
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
if (data === null) {
// get请求
client.open("GET", url);
client.onreadystatechange = handler;
client.send(null);
} else {
// post请求
client.open("POST", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Content-Type", "application/json");
client.send(JSON.stringify(data));
// client.send(data);
}
});

return promise;
};

// Promise改写
new Promise((resolve, reject) => {
reqJSON(localUrl + "/first", null)
.then(json => {
console.log('Contents: ', json);
// 上面reqJSON()封装的请求里面的resolve代表的是封装的成功,两者不一样
// 下面的resolve代表请求成功后成功(可以理解为收到200后)
resolve();
// reject("first接口报错");
})
})
.then(data => {
return new Promise((resolve, reject) => {
reqJSON(localUrl + "/second", null)
.then(json => {
console.log('Contents: ', json);
resolve();
// reject("second接口报错");
})
})
})
.then(data => {
return new Promise((resolve, reject) => {
reqJSON(localUrl + "/login", userData)
.then(json => {
console.log('Contents: ', json);
resolve();
// reject("login接口报错");
})
})
})
.catch(err => {
console.log("捕捉到error:", err);
})

上面代码的上部分是使用Promise实现Ajax操作,下部分则是三个请求,虽然还是有点丑,可以进一步封装,但是确实把嵌套打开了。并且最重要的是异常捕获没有问题,resolve代表可以继续往下进行,reject则表示出错,只需要在最后面写上catch既可以全部捕获

Promise实现原理

这里照着链接敲了一遍,然后自己打断点跑了几遍,也算是理解了Promise的大致实现流程。

首先分析一下,Promise一共三种状态,pendingresolvedrejected三种状态,然后常用方法两种thencatch。那么我们要做的就是将这些逐个实现。

首先就是构造函数的编写,这里就不贴代码了,想看代码得可以去上面链接自己敲一遍。由上述可得,内部属性status用来存三种状态,data用来存resolve传入的数据。然后Promise接收的参数是一个函数,函数有两个参数,分别是resolvereject两个方法。因此,构造函数还需要实现resolvereject两个方法,方法功能是调用后将状态改变,以及将resolvereject的参数存入data。这里有一个要注意的地方就是,Promise的状态一经改变,就会凝固,不会再改变了。所以这里要注意一下。

然后就是then方法的实现,then方法接收两个参数,一个是成功回调函数,一个是失败回掉函数。而且这里的then返回的是新的Promise实例,但是属性还是之前的数据,原因是,假如是同一个对象,那么假如thenpromise抛出一个异常的话,状态就变成了rejected,这就违背了Promise状态一经改变就不会再变的原则。之后then内部会调用传入的回掉函数,并改变此时的promise对象属性。

需要注意的一点是,new Promise(resolve => resolve(8)).then().then((value) => console.log(value)})这种情况下需要值穿透,方法就是假如then不传参数的话,我们默认给它一个参数,让其return自己,就能实现值穿透。

catch方法的话实现起来就比较简单了,直接调用then(null, onRejected)即可。

这里有一点要注意的,就是之前构造函数里还有两个属性_self.onResolvedCallbacks_self.onRejectedCallbacks这两个属性分别是数组,存的是函数。之前一直不了解两个属性的作用。后来仔细查看之后,这两个属性存的是状态pending情况下的回调。那么是什么意思呢,意思就是说当触发then方法的时候,status有可能是pending状态,那么这个时候并不知道是要调用成功回调还是失败回调。拿方案就是都存下来,当后面状态改变的时候,当触发resolve时,会遍历callback数组,并执行函数。我们可以用一段代码,打断点测试一下,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const promise = new MPromise(function(resolve, reject) {
console.log("MPromise");
});

promise
.then(response => {
console.log(response);
console.log("then回调");
})

promise.resolve("second");

promise
.then()
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
})

上述代码的第一个then时,状态还是pending,就会触发存入callback的操作。而且后面有一个无参数的then,可以测试值穿透的情况。

那么实现原理大概就是这样了,自己动手写一遍就很清楚了。其实我上面讲的肯定很乱,没有代码空口白说,直接看估计也看不懂。主要是为了自己梳理一下思路吧,想具体了解原理的请点上述链接自行查看。

Generator改写

Generator封装了多个内部状态,其使用next()来继续运行,使用throw()来抛出错误。内部使用yield来定义内部状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function* reqFun() {
try {
yield reqJSON(localUrl + "/first", null)
.then(json => { console.log('Contents: ', json); it.next();})

yield reqJSON(localUrl + "/second", null)
.then(json => { console.log('Contents: ', json); it.throw(new Error('error'));})

yield reqJSON(localUrl + "/login", userData)
.then(json => { console.log('Contents: ', json); it.next();})
} catch(e) {
console.log(e);
}

}

let it = reqFun();
it.next();

代码如上图,发送请求依然是用上面Promise封装的ajax操作,优点代码更加简洁,而且也解决了在外部使用一个try/catch就能捕获内部所有状态

其中需要手动执行函数,并使用next()让其继续运行。当然可以使用co模块让其自动运行,这里就不再赘述

async函数改写

async函数使异步操作更加方便,简单讲就是Generator函数的语法糖,其内置了执行器,会自动执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function reqFun() {
try {
await reqJSON(localUrl + "/first", null)
.then(json => { console.log('Contents: ', json);})

await reqJSON(localUrl + "/second", null)
.then(json => { console.log('Contents: ', json); throw new Error("error");})

await reqJSON(localUrl + "/login", userData)
.then(json => { console.log('Contents: ', json);})
} catch(e) {
console.log(e);
}
}

reqFun();

和上面Generator改写的很相似,async有更广的适应性,其返回值是Promise,也就意味着定义的reqFun()函数可以继续then()或者catch(),可以构造更加合理的代码结构

总结

文中只是整理了一下基本的使用,以及自己的一些见解吧,也参考了很多博客。也算是对ES6的一些内容的进一步实践吧