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