关于ES6生成器,你应该知道的地方

摘自你不知道的JavaScript中卷


前言

通过前面一章的学习,我们知道了Promise为我们解决了回调带来的不信任问题以及基于回调的异步嵌套不符合人类大脑的规划方式,最终通过.then的方式来编写异步代码,即便是.then的形式,如果连续的编写也会带来另一种流程控制风格的问题,那么有没有其他更好的,看似同步的异步流程控制风格呢?

记得在一篇文章中看过,有人说最好的异步就是没有异步

Generator

ES6中引入的一种新的函数类型,这类函数不符合传统函数从运行到结束的特性,可中断

生成器函数如何定义
function *foo (x) {
    x++;
    let y = yield x;
    console.log(y);
}

可以看到,生成器函数在命名上比传统函数多了一个*号,且在方法体内有yield关键字

如何调用生成器函数
var foo = foo(2);// 1
var v = foo.next();// 2
console.log(v.value);// 3
foo.next(10);// 4
  1. 上面的第一步并没有执行生成器函数foo,而只是构造了一个迭代器Iterator,通过这个迭代器实例来控制生成器函数的执行

  2. 中的第一次next()调用实际上是启动了生成器,并执行到第一个有yield表达式的地方暂停住,next()第一次调用结束,此时*foo()仍在运行并且时活跃的状态,只是其处于暂停状态,

  3. 注意这里2中调用后的返回值v,其实际上表示的是*foo()函数体内yield关键字后面的值的封装,为什么说是值得封装,因为next()函数总是返回一个对象,对象中包含两个属性,一个value,一个done

value就是yield关键字后面表达式的值,而done表示的是foo迭代器是否迭代完成的标识:true或false

  1. 第四步中next()是可以传参的,由于之前的执行*foo()暂停在了第一个yield关键字的位置,故第四步中的next传参实际上是将传参的值赋值给了那第一个yield关键字

至此我们知道了ES6生成器可以通过返回的迭代器实例来自由控制其执行流程,并可以通过yield关键字实现双向数据传输(即通过生成器暴露值出来以及向生成器函数传值进去)

代码执行结果

3
10
{value: undefined, done: true}

异步迭代生成器

明白了生成器的执行流程,那么我们将异步加入生成器中看看效果,直接上代码

// 定义一个函数,模拟异步请求返回
function timeout () {
    setTimeout(() => {
        console.log('耗时三秒模拟异步请求');
        it.next({data: 'success', code: 0});
    }, 3000);
}

// 定义一个生成器函数
function *main() {
    var value = yield timeout();
    console.log(value); 
}

// 启动生成器
var it = main();
it.next();

下面分析一下以上代码的意图以及流程

意图:

  1. 定义的timeout函数一般就是我们实际项目中请求后端数据接口的代码,在ajax请求回调函数成功时将后端数据传入迭代器的next()中

  2. *main生成器函数一般是我们在实际项目中调用封装了ajax请求函数的客户端代码片段

  3. 启动生成器函数

流程:

  1. 调用生成器函数main返回一个迭代器实例it

  2. 启动迭代器

  3. 迭代器执行timeout()函数,并暂停在yield表达式处

  4. timeout函数执行,等待3秒后执行打印'耗时三秒模拟异步请求',并继续执行迭代器的next方法,并向其传入参数:{data: 'success', code: 0}

  5. 迭代器从yield处激活重新执行,并打印传入的参数对象,执行流程结束

大家重点看一下这里的代码片段

var value = yield timeout();
console.log(value); 

timeout函数是一个模拟请求远程的异步方法,这里用了两行代码,就获取到了远程调用的结果值,而且是看似同步的形式获取的

这不就是我们说的好的异步就是没有异步的概念吗?

而且最重要的一点是以上代码并没有阻塞整个程序代码,而只是阻塞或暂停了生成器本身

通过以上的小例子,我相信你已经明白了生成器函数存在的意义了

生成器函数的意义

我们拥有了在生成器函数内部看似完全同步的代码逻辑,且yield背后的表达式内部可以完全异步形式调用

借用书中的一句话:

从本质上来说,我们把异步作为实现细节抽象了出去,使得我们可以以同步顺序的形式追踪流程控制:

发出一个Ajax请求,等他完成之后打印出相应结果


生成器 + Promise组合

ES6中最完美的世界就是生成器(看似同步的异步代码)和 Promise(可信任可组合)的组合

请好好理解以上这句话的意义所在,这是目前ES6中最精华的一句话

以上代码示例中,我们利用了生成器去yield一个异步Ajax请求,而Ajax回调存在的问题就是可信任问题,为了解决可信任的问题,我们需要利用Promise

改进
  1. 让封装了ajax请求的方法返回一个Promise,且不需要在原代码中控制迭代器的next(),仅仅只是返回一个Promise即可

  2. 迭代器内部代码不需要做任何的变动,也就是说yield关键字后面的表达式将会暴露出一个Promise对象出来

  3. 启动迭代器的代码变动:

     var it = main();
     var p = it.next().value;
     // 此处编写Promise的决议后回调代码
     p.then(function fulfilled (text) {
         // 将Promise决议完成的后端结果传入迭代器的第二个next()中
         it.next(text);
     }, function rejected(error) {
         it.throw(error);
     });
    

通过以上的改进,我们可以看到,加入了Promise的代码解决了回调的可信任问题,且生成器函数内部代码不需要做任何的改动即可

在生成器内部,不管yield出来的是什么值,都只是透明的,我们不需要关心它

通过yield暴露出来的Promise之后,我们应该对其做什么呢?

  • 对这个Promise做决议监听,在决议完成回调中恢复生成器的执行

  • 在决议拒绝回调中向生成器抛出一个带有拒绝原因的错误

再次改进

通过以上分析我们知道了,其实我们需要编写的代码部分只有:

  • 请求后端数据接口返回Promise实例方法

  • 定义一个生成器函数,用于在内部yield 出返回的Promise实例

  • 启动迭代器代码,对next()返回的Promise实例添加then回调函数,并在决议成功和决议拒绝时恢复生成器或抛给生成器一个错误

细细分析以上三个步骤,我们发现:第一步,我们是必须要编写的,关系到请求的后端接口

第二步中也是需要编写的,而第三步其实是业务无关的,也就是说我们可以编写出一个工具来做第三步中所做的工作,比如asynquence库及其runner(...)

ES7 async 和 await?

前面通过生成器yield出来一个Promise实例,然后利用Promise的决议监听去控制迭代器的执行,直到结束的形式是一种非常有用的方式,若我们能够无需使用工具辅助函数(run(...))就能够实现就好了

关于这一点,好消息是目前主流浏览器Chrom等已经强势支持了这一方面的增强语法形式async await

也就是我们平常在代码中编写的A/A形式代码

1:编写异步函数返回Promise实例

2:无需定义生成器形式函数,采用在函数前增加async关键字代替*号,以及在函数内部添加await关键字代替yield的形式来处理

3:不在需要yield出一个Promise实例后再去编写决议监听回调函数处理生成器的恢复执行或抛给生成器一个异常错误,而是利用更强大的await关键字直接等待1中Promise的决议

4:也就是说,如果你await了一个Promise, async 函数就会自动获知要做什么,它会暂停该函数(就像生成器暂停一样),直到该Promise决议

5:async函数在调用后会自动返回一个待决议的Promise,在函数完全结束之后,该Promise会自动决议
语法糖?

从本质上来说,async和await就是生成器加Promise的语法糖,将这两个ES6世界中最美好的部分结合起来,优雅并实际的解决了回调方案存在的主要问题

总结

以上就是本人对于ES6中生成器的一些简单理解,有错误的地方还请指出修正

你可能感兴趣的:(关于ES6生成器,你应该知道的地方)