JavaScript异步代码处理方案

异步代码的问题

​ 不得不说在早期的js中,处理异步代码是一个极具挑战性的问题,也是一个即为头疼的问题,不是说编写异步代码有什么难度,而是编写出维护性与阅读性并存的异步代码有难度,特别是async和await还没有出现的时代,经常在代码中出现一些回调地狱。所以今天我们就结合具体场景来看看异步代码的演进过程,了解历史才能更接近真相。

准备工作

  1. 首先很简单,我们就模拟实际开发中经常出现的场景,有的时候我们发送网络请求,需要拿到上一次网络请求的返回值,作为下一次发送网络请求的参数。我们将这个场景简化一下,定义一个Promise

    1
    2
    3
    4
    5
    6
    function request(url) {
    return new Promise((resolve, reject) => {
    resolve(url);
    });
    }
    //我们定义一个函数,返回一个Promise,Promise内部做的事情也很简单,直接将传进来的参数丢出去

第一种解决方案

​ 结合实际开发场景,出现了下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
//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);
});
});
});

​ 于是我们的代码演变成这个样子,也不是不能看,但是不得不说阅读性极差,作为一个合格的前端开发工程师,并不能允许自己的代码频繁出现这样的结构,如果依赖上一次网络请求的次数更多,那么最后的代码将会是惨不忍睹。不得庆幸的是人是会慢慢进步的,于是在时间的推移下,上面的代码进行的进化。

第二种解决方案

1
2
3
4
5
6
7
8
9
10
11
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方法,这样就形成了一个链式调用,现在代码看起来阅读性就比第一种方案好多了

第三种方案

  1. 第三种方案就有点难以理解,我们需要借助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方法就能拿到这次网络请求的返回值了
  2. 然后在考虑下传递参数的问题,因为我们不是拿到返回值就完事了,如果是这样何必绕那么一大圈。所以我们还需要知道生成器是可以传递参数的,简单来说就是第二次调用next传递的参数能作为生成器的第一次yield 的返回值拿到,看代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function* 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. 首先要使用第三方包肯定要先安装

    1
    npm install co
  2. 然后导入

    1
    const co = require('co')
  3. 进行使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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

    这就完事了,就这么简单粗暴,co本质上就是我们在上面编写的递归函数,在node_modules文件夹中找到co,可以看到源码也就200多行,也容易看懂,而且大部分都是在做边界判断(edge case)

第五种方案(最终方案)

​ 学习了那么多种方案,现在我们终于能引出最终的方案,也就是ES8(ES2017)推出的新特性:async/await

1
2
3
4
5
6
7
async 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对代码进行一个转换,所以开发的时候也可以大胆地使用。