异步之生成器

140 阅读4分钟

0. 前言

在JavaScript中,代码几乎普遍依赖一个假定:一个函数一旦开始执行,就会运行到结束,期间不会有其他代码能够打断它并插入其间。然而,在 ES6 中,引入了一种新的函数类型,它可以使得函数在运行过程中被打断。这种类型被称为生成器。在这篇文章中,我们就来聊一聊这个新的神器——生成器(Generator),以及它是如何来解决异步问题的。在此之前,我们需要明确的一点是生成器其实并不是为解决一部问题而产生的,但是它又天生非常适合解决异步问题,也就是说生成器本身并不解决异步问题。

1. 生成器

我们首先来看生成器的使用方式:

var x = 1;
function *foo() {
    x++;
    yield;
    console.log('x:', x);
}

function bar() {
    x++;
}

这段代码中,函数foo()就是一个生成器的类型,而字段 yield 的功能是打破函数内部运行。这就是我们在文章的开始提到的生成器可以打破函数内部的运行。现在,我们要让借助迭代器让这段代码运行起来。

var it = foo();

// 这里启动foo()
it.next();
x;           // 2
bar();       // 3
it.next();   // x:3

这段代码的运行过程是这样的:

  • 首先, it = foo() 运算并没有执行生成器 *foo() ,而是构造了一个迭代器,这个迭代器会控制它的运行
  • it.next()这段代码启动了生成器,当生成器运行到yield字段时,会暂停运行,保留现场。在我们这个例子当中,被保留的现场是变量x,在执行了x++之后,它的值变成了2.
  • 此时,运行bar()函数,同样执行了x++语句,此时的x值变成了3.
  • 最后,it.next() 使得生成器函数从暂停出恢复运行,最后打印的结果是x:3

从这段代码的运行过程,我们也看得出来生成器函数在运行的过程中被打断了。在运行到 yield 时,中间执行了 bar() 函数才接着继续执行完成的。这个例子所展示的就是生成器。

在例子中,我们除了生成器,还提到了另一个概念——迭代器。我们使用它来控制生成器的运行。在这里,我们简单介绍一下迭代器的概念,关于迭代器详细的内容还请看这里的内容. MDN中的定义为:

在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。

2. 生成器 + Promise 解决异步

我们知道,生成器本身并不能解决异步问题,能够解决异步问题的就是我们之前提到的回调和Promise。而Promise又被称为异步问题最优雅的解决方案。不管怎么说,在这里我们解决异步问题的方案是Promise + 生成器。关于Promise,可以看这篇文章

俗话说:“没有对比就没有伤害”。我们先来看纯粹的Promise版本的ajax请求:

function foo(url) {
    return request(
        url
    );
}

foo(url)
    .then(res => {
        console.log('response:', res);
    }, err => {
        console.log('error:', err);
    });

然后,我们再来看支持生成器版本的ajax请求:

function foo(url) {
    return request(
        url
    );
}

function *main() {
    try {
        var text = yield foo(url);
        console.log(res);
    } 
    catch(err) {
        console.log(err);
    }
}

显然,使用生成器版本的代码要简介明了的多。不需要使用then()方法就可以完成异步的实现,甚至不需要改动原来的同步代码就可以实现异步,丑陋的then已经被屏蔽掉了。然后,我们使用迭代器来运行生成器就可以了。

var it = main();
var p = it.next().value;

p.then(
    text => { it.next(text); },
    err => { it.throw(err); }
);

3. async/await

如果你 await 了一个 Promise, async 函数就会自动获知要做什么,它会暂停这个函数(就像生成器一样),直到 Promise 决议。在这里,我们需要知道async...await 解决异步问题的方案其实就是生成器+Promise的语法糖。具体的使用方式如下:

async function() {
    let a = await ajax(url) 
    //....
}

这种方法可以说是异步解决方案中最简洁的,也是书写起来最接近同步代码的解决方案了,在很多场合中,我们都会直接使用这种方案来完成异步代码的编写。但是要理解 async...await 内部究竟发生了什么,还是需要仔细理解Promise和生成器都是怎样工作的。