异步编程之Generator(2)——剖析特性

系统 1872 0

异步编程系列教程:

  1. (翻译)异步编程之Promise(1)——初见魅力
  2. 异步编程之Promise(2):探究原理
  3. 异步编程之Promise(3):拓展进阶
  4. 异步编程之Generator(1)——领略魅力
  5. 异步编程之Generator(2)——剖析特性
  6. 异步编程之co——源码分析

Generator基础


继上一篇见识过其配合promise带来的超爽的异步编程体验,我想应该大部分同学都会想好好看一下,到底这个Generator是什么?接下来我们会对Generator的特性进行剖析,让我们对接下来学习 co 源码打个扎实的基础。

起源

我们首先得知道,Generator一开始并不是用来做异步编程的,是后来的大牛们挖掘了它的特性,让它在异步编程里大放异彩。其实Generator是生成遍历器的构造器,ES6定义了一个遍历器的接口Iterator。任何数据结构满足Iterator接口,都可以统一实现遍历操作。一步一步的调用 next() 或者 for..of 循环都可以遍历实现Iterator接口的数据结构。

我们简单说一下遍历对象的 next() 是怎样的:

  1. 第一次调用 next() 会直接指向第一个数据的位置,然后返回数据的信息。结构是这样的: {value: AnyType, done: Boolean} value 属性是指该数据的值, done 则是标志是否已经true,结束了。
  2. 再一次调用 next() 则指向下一个数据,返回相应的数据信息。
  3. 重复第二步,一直到数据结束,返回 {value: undefined, done: true} 。则表示遍历已经全部完成。

这就是Iterator最基本的实现,当然这里是很片面的,若要展开说,基本又是一大篇文章可以写。这里就直接给出阮一峰老师关于Iterator的文章: 10. Iterator和for...of循环

定义

在我们知道了Generator生成的遍历对象是什么之后,我们看一下如何定义这样的Generator函数。对上一篇有印象的同学,应该记得函数标识符后面有一个诡异的星号 function* () 。其实这个星号在括号前也是没关系的,这里我是参考了 co 源码的。我们一旦定义了一个带星号的函数之后,用这个构造器生成的对象在harmony模式里就成了Generator对象(下面我会称其为遍历器)。我们可以测试一下一段代码。

    
      var toString = Object.prototype.toString;

var Generator = function* (){
    yield "hello";
    yield "world";
};

var gen = Generator(); // 可以省去new来创建对象

console.log(toString.call(Generator)); // [object Function]
console.log(toString.call(gen));       // [object Generator]
    
  

这样我们通过调用特殊定义的Generator构造器,生成一个遍历器([object Generator])。那我们要遍历的话必须得知道遍历的每个成员, yield 就是用来定义遍历成员的。也就是说,遍历器进行遍历的时候会以 yield 为间隔,一个 yield 一个成员,不断往下走直到不存在下一个 yield

在上面的例子中,就是第一次遍历到 yield 得到"hello",第二次继续执行遍历操作到 yield 得到"world",最后再执行就发现没有了,也就是 done: true 结束遍历。

接下来我们会详细说一下,遍历器是遍历的各种特性。

Generator特性


遍历

我们需要执行遍历,首先就是要得到遍历器。前面也说过了,就是调用Generator构造器生成的。然后该遍历器会有一个方法 next() 用来进行遍历操作,并且每一次的操作都会在 yield 处停止,并等待下一次的 next() 指令。我们看一看刚才的代码:

    
      var Generator = function* (){
    yield "hello";
    yield "world";
};

var gen = new Generator();

console.log(gen.next()); // { value: 'hello', done: false }
console.log(gen.next()); // { value: 'world', done: false }
console.log(gen.next()); // { value: undefined, done: true }
    
  

我们可以看到最后当 done: true 时, value 是undefined。其实我们return出去一个值,就会成为该 value 的值。其实换一个角度更加有意思,就是当你return出一个值,这个值必定是 done: true 。我们可以改一下上面的例子:

    
      var Generator = function* (){
    yield "hello";
    return "world";
    yield "!";
};

var gen = new Generator();

console.log(gen.next()); // { value: 'hello', done: false }
console.log(gen.next()); // { value: 'world', done: true }
console.log(gen.next()); // { value: undefined, done: true }
    
  

我们可以看到,如果遍历器去找感叹号的 yield 话,应该是 value: '!' 。但是因为提前return结束了遍历器,所以最后得到了 { value: 'world', done: true }

yield传值

我们知道了每一次遍历器执行到 yield 处后,会把值放在一个对象中的属性中返回出去。但是我们在Generator构造器里怎么利用这个值呢?其实我们可以为遍历器的 next(res) 传入一个参数,这个参数将会成为这一次 yield 的值。乍一看,好像不大清楚,看看代码就懂了。

    
      var Generator = function* (){
    var hello = yield "hello";
    console.log(hello);           // hi
    var world = yield "world";
    console.log(world);           // undefined
};

var gen = new Generator();

var first = gen.next("nothing");
var second = gen.next("hi");
var third = gen.next();
    
  

我们第一次 next() 相当于启动器,这个时候传入任何参数都是被忽略的,因为这个参数无法作为上一个 yield 的值(没有上一个)。到我们第二次的 next("hi") ,传入了一个"hi"字符串,这个参数就成为了 yield 的值,直接赋值给hello变量并打印出来。我们最后一个world变量是undefined,是因为 next() 并没有传入任何参数。可以这么说,每一次遍历器遍历得到的成员的值,和 yield 的值是没有必然联系的。

所以我们看代码的执行顺序也是很有趣的一件事,遍历器会执行到语句 yield 右侧即停止。等到下一次 next() 启动,然后才会根据 yield 得到的值,对语句左侧变量进行赋值。这样想的话,如果我们下一次 yield 语句,依赖第一次的值,我们就需要在 next() 里传入上一次的 value 。我们对上一次的代码做个小小的添加。

    
      var first = gen.next("nothing");
var second = gen.next("hi");
var third = gen.next(second.value); //构造函数的world变量值也会是"hi"。
    
  

这个是Generator非常重要的特性,下去要好好实践一番,加深印象。接下来 co 源码分析,这个特性配合promise可以放华丽的大招。

遍历遍历器里的遍历器

我起这个标题挺有意思的,哈哈哈。其实就和递归栈差不多,也就是说,当 yield 的是另一个遍历器,那么代码会进入到另一个遍历器里,直到结束后,才交回代码控制权。看一看咯:

    
      var Generator = function* (){
    yield  "hello";
    yield *anotherGen;
    yield "world";
    return "hello world";
};

var AnotherGenerator = function* (){
    yield "强势插入!";
    yield "不给hello world!";
}

var gen = new Generator();
var anotherGen = new AnotherGenerator();

console.log(gen.next()); // { value: 'hello', done: false }
console.log(gen.next()); // { value: '强势插入!', done: false }
console.log(gen.next()); // { value: '不给hello world!', done: false }
console.log(gen.next()); // { value: 'world', done: false }
console.log(gen.next()); // { value: 'hello world', done: true }
    
  

当我们需要遍历一个遍历器,那么 * 也是需要的,可以参考一下上面。

总结


我们知道了遍历对象遍历时得到的什么,还有 next(res) 传入参数有什么用,这对接下来的分析有着至关重要的作用。到这里,对Generator分析已经是差不多了。如果想要更深入了解的,可以去阮老师的博客看一看: 11. Generator函数

接下来一篇文章就是对 co 源码的分析,先预习和复习一些东西吧。我们回顾一下promise,我们在将一个异步操作promise化后,当我们调用这个异步操作,我们会得到一个promise对象。所以我们可以想象一下:

  1. 我们调用遍历器的 next() 得到该异步的promise对象
  2. 在promise对象的 then() 中的 resolve 对数据进行处理
  3. 把数据作为参数传入 next(res) ,进行下一次异步操作
  4. 直到迭代器的 done: true ,结束遍历。

这样我们就可以一环扣一环的将Generator函数里的异步操作进行迭代,形成一种异步编程同步写法的优良体验。当然我们这里不会详细说,如何去实现,因为我会在下一篇好好讲讲。

异步编程之Generator(2)——剖析特性


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论