异步代码
JavaScript异步代码处理方案
异步代码的问题
不得不说在早期的js中,处理异步代码是一个极具挑战性的问题,也是一个即为头疼的问题,不是说编写异步代码有什么难度,而是编写出维护性与阅读性并存的异步代码有难度,特别是async和await还没有出现的时代,经常在代码中出现一些回调地狱。所以今天我们就结合具体场景来看看异步代码的演进过程,了解历史才能更接近真相。
准备工作
首先很简单,我们就模拟实际开发中经常出现的场景,有的时候我们发送网络请求,需要拿到上一次网络请求的返回值,作为下一次发送网络请求的参数。我们将这个场景简化一下,定义一个Promise
1
2
3
4
5
6function request(url) {
return new Promise((resolve, reject) => {
resolve(url);
});
}
//我们定义一个函数,返回一个Promise,Promise内部做的事情也很简单,直接将传进来的参数丢出去
第一种解决方案
结合实际开发场景,出现了下面的代码
1 | //1. 我们首先传入一个url |
于是我们的代码演变成这个样子,也不是不能看,但是不得不说阅读性极差,作为一个合格的前端开发工程师,并不能允许自己的代码频繁出现这样的结构,如果依赖上一次网络请求的次数更多,那么最后的代码将会是惨不忍睹。不得庆幸的是人是会慢慢进步的,于是在时间的推移下,上面的代码进行的进化。
第二种解决方案
1 | const result = request("http://studyvue.cn"); |
首先我们需要知道Promise的返回值也是一个Promise,当我们把一个字符串作为Promise的参数传递进去的时候,会自动调用Promise.resolve方法,当然传递其他类型也会有不同的处理方式,比如说传递一个函数,会根据实际情况调用resolve或者是reject方法,这篇文章也不是讲Promise的,就不多赘述了。看向我们上面的代码,将一个Promise传递一个字符串后返回出去,我们就可以在后面继续调用then方法,这样就形成了一个链式调用,现在代码看起来阅读性就比第一种方案好多了
第三种方案
第三种方案就有点难以理解,我们需要借助Promise+生成器来完成,先写个简易版的,来看下代码的执行顺序
1
2
3
4
5
6
7
8
9//我们先定义一个生成器
function* foo() {
yield request("http://studyvue.cn");
}
const result = foo();
result.next().value.then(res => {
console.log(res);
});- 首先我们先定义一个生成器
- 然后调用这个生成器,拿到返回值,返回值是一个迭代器,不知道迭代器的可以看我上一篇文章,我个人觉得还是讲的很全面的
- 然后调用next方法拿到一个对象,这个对面长这个样子:{ value: Promise { ‘http://studyvue.cn‘ }, done: false }
- 我们可以看到我们需要的东西在value属性上,并且它的值是一个Promise,于是我们拿到value的值,并且调用then方法就能拿到这次网络请求的返回值了
然后在考虑下传递参数的问题,因为我们不是拿到返回值就完事了,如果是这样何必绕那么一大圈。所以我们还需要知道生成器是可以传递参数的,简单来说就是第二次调用next传递的参数能作为生成器的第一次yield 的返回值拿到,看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14function* foo() {
const result1 = yield request("http://studyvue.cn");
const result2 = yield request(result1);
yield request(result2);
}
const result = foo();
result.next().value.then(res => {
result.next(res + "/coderwei").value.then(res => {
result.next(res + "/19").value.then(res => {
console.log(res);
});
});
}); 这种方式看起来还是一种回调地狱,但是问题不大,我们可以看看具体处理逻辑,其实都差不多的,都是调用next方法传递参数,然后拿到value属性的值调用then方法而已,所以这里我们可以写成一个递归,当然这种代码不会写也没关系,npm仓库里就有对应的包帮我们执行我们写的生成器(co),递归的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 3. 改进 ---->递归
function* foo() {
const result1 = yield request("http://studyvue.cn");
const result2 = yield request(result1 + "/coderwei");
const result3 = yield request(result2 + "/19");
console.log(result3);
}
function execGenerator(generatorFn) {
const generator = generatorFn();
function exec(res) {
const result = generator.next(res);
if (result.done) {
return result.value;
}
result.value.then(res => {
exec(res);
});
}
exec();
}
execGenerator(foo); //http://studyvue.cn/coderwei/19
第四种方案
第三种方案的递归写起来还是有点麻烦的,所以我们就可以借助第三方库。这个时候就体现了一门语言有一个完善的生态和社区是多么重要。npm的仓库有一个叫co的包在处理这种代码时很出名的,他也是大神TJ的作品,同时他也是express/koa/n/commander的作者,在async和await还没出来之前,co在处理这种代码几乎是必备的。
首先要使用第三方包肯定要先安装
1
npm install co
然后导入
1
const co = require('co')
进行使用
1
2
3
4
5
6
7
8
9function* foo() {
const result1 = yield request("http://studyvue.cn");
const result2 = yield request(result1 + "/coderwei");
const result3 = yield request(result2 + "/19");
console.log(result3);
}
co(foo);
// http://studyvue.cn/coderwei/19这就完事了,就这么简单粗暴,co本质上就是我们在上面编写的递归函数,在node_modules文件夹中找到co,可以看到源码也就200多行,也容易看懂,而且大部分都是在做边界判断(edge case)
第五种方案(最终方案)
学习了那么多种方案,现在我们终于能引出最终的方案,也就是ES8(ES2017)推出的新特性:async/await
1 | async function foo() { |
是不是很神奇,只是将*删掉,然后在函数前面加上async,然后将所有的yield换成await,结果是一模一样的。虽然这两个关键字是ES8推出的,但是目前的开发中我们都会使用babel对代码进行一个转换,所以开发的时候也可以大胆地使用。










