Es6 in Depth: Generators

ES6 In Depth: Generators

我对今天的文章感到非常的兴奋,今天,我们将讨论ES6中最为魔法的特性。

从“魔法”这词我想表达什么?对于初学者来说,这个特性是不同于现有JS中的,它一开始就是神秘的。从某种意义上说,它还是语言内部的正常行为(In a sense, it turns the normal behavior of the language inside out)。如果这还不够魔法,我都不知道怎么说了。

另外,这个特性重点还在于,其以简明的代码来消除邪恶代码中的callback。(JS常见的多层callback, 写nodejs会很是恶心。)

是我有点夸大了?让我们开始进入,然后由你自己来作出判断吧。

ES6 生成器介绍

什么是生成器?

让我们开始用一个例子来看一下:

1
2
3
4
5
6
7
8
function* quips(name) {
  yield"hello " + name + "!";
  yield"i hope you are enjoying the blog posts";
  if (name.startsWith("X")) {
    yield"it's cool how your name starts with X, " + name;
  }
  yield"see you later!";
}

这代码来自a talking cat,它可能是现今网络最为重要的一种应用。(继续,点击,一起玩耍吧。当你感觉到完全懵时,回到这里看解释。)

这代码看起来有几分类似函数,是不是?这个称为generator-function(生成器函数),它拥有许多和函数相同的地方。但是,你现在必须清楚以下两个不同点:

  • 一般的函数以function开头,生成器函数是function*
  • 在生成器函数中,yield是关键字,其语法更像是return。区别在于函数(甚至是生成器函数)只能return一次,而生成器函数可能yield多次。生成器中的yield会延缓执行,所以它可以在以后重新唤起。

这就是一般函数和生成器函数的一个很大的不同点。一般函数不能内部暂停,但生成器函数可以。

生成器做什么

当你调用quips()生成器函数时,会发生什么呢?

1
2
3
4
5
6
7
8
9
10
> var iter = quips("jorendorff");
  [object Generator]
> iter.next()
  { value: "hello jorendorff!", done: false }
> iter.next()
  { value: "i hope you are enjoying the blog posts", done: false }
> iter.next()
  { value: "see you later!", done: false }
> iter.next()
  { value: undefined, done: true }

你可能会习惯地按照普通的函数去推断其是怎么运行的。当你调用时,它会马上运行,它一直运行直到碰到return或者throw,这是所有JS程序大大的老习惯。

调用一个生成器的方法类似于quips("jorendorff"),但是,当你调用一个生成器时,它并不立即运行,而返回的是一个暂停的生成器对象(在例子中是iter)。你可以认为这个生成器函数是一个函数调用,它这时是冻结的。具体来说,仅在运行第一行代码之前,它在生成器函数开始时是冻结的。

每次你调用生成器对象的.next()方法时,函数自已会开始解冻,并且直到下一个yield时才会停止。

这就是上面为什么我们每次调用iter.next(),却得到不同的字符串值的原因。这些值来源于quips()函数中的yield表达式。

在最后一次调用iter.next(),我们来到了生成器函数的末尾,所以.done域的结果是true。到达函数的末尾时返回值是undefined,这就是.value域的结果是undefined

现在可能合适回到 the talking cat demo page 把这代码玩起来了,试试将一个yield放到循环里面,会发生什么呢?

从技术的角度来说,每次生成器yield时,生成器的栈架构中,yield的本地变量、参数、临时变量、执行的位置都会从此栈架构中移除。但是,这个生成器依然会对此栈架构保持着引用(或者是复制体),所以之后的.next()调用时会再次激活生成器并继续执行。

这里需要指出的是生成器并不是多线程的在多线程的语言中,多个代码块可以在同一时间一起运行。这会引起不同条件的资源竞争、不确定性、非常好的性能。生成器一点都不像这样,当生成器运行时,它只能运行于调用者的那个线程中,这个执行过程是有序并确定的。并不像系统线程,当生成器碰到yield时,它只会在其内部的相应位置暂停起来。

好了,我们现在知道生成器是什么了。我们知道生成器运行、暂停、重新执行。现在有个大问题?怎么使得这强大的能力变得可用呢?

生成器是迭代器

上周,我们看到的ES6迭代器不仅仅是个内置的类,它们还是这新语言允许扩展的地方。你通过实现 [Symbol.iterator]().next()方法来创建自己的迭代器。

但是,实现这个接口多少都会需要些工作,让我们来看一下在实践中是怎么实现一个迭代器的。例如,让我们来实现一个简单的值域来作简单地递增一个数值,就像旧时的for(:;)循环:

1
2
3
4
// This should "ding" three times
for (var value of range(0, 3)) {
  alert("Ding! at floor #" + value);
}

这里有个解决方案,就是使用ES6的类。(如果对类的概念语法不清楚时,不用担心,我们会在以后的文章中进行说明的。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class RangeIterator {
  constructor(start, stop) {
     this.value = start;
     this.stop = stop;
  }
  [Symbol.iterator]() { return this; }
  next() {
     var value = this.value;
     if (value < this.stop) {
       this.value++;
       return {done: false, value: value};
     } else {
       return {done: true, value: undefined};
     }
  }
}


// Return a new iterator that counts up from 'start' to 'stop'.
function range(start, stop) {
  return new RangeIterator(start, stop);
}

点击这里看效果

这就是如 Java 或者 Swift 的实现方式。它不是很坏,但是它又不是那么微不足道,而不用注意。这代码没有bug?这并不好说。我们试图模拟原来的for(:;)循环,但是看起来不并像,相反,迭代器的协议强制我们去拆散这循环。

看到这里,你或许对迭代器有些许的冷淡了。迭代器好用,但不好实现。

为了让迭代器更为容易地构建,我们不可能向你介绍一种少见、奇怪的新的JS语言流结构。但是,自从我们已有了生成器,我们能不能用它们来实现?让我们试一下吧:

1
2
3
4
function* range(start, stop) {
  for (var i = start; i < stop; i++)
     yield i;
}

点击这里看效果

以上的四行代码的生成器,相比于之前的包含有RangeIterator类的23行Range()实现,复杂度得到了降低。这可能就是生成器是迭代器的原因。所有的生成器都有内置的.next()[Symbol.iterator]()的实现。你只需要写循环的行为就行了。

离开生成器来实现迭代器就像是被强制用被动语态来写一整封长长的邮件。如果你不能通过简单的语言来说明你想表达的意思,那么相反你最终说的会变得很费解。由于没有使用循环语法,必须要用函数式的代码来完成循环,使RangeIterator变得又长又显怪异。因此,生成器就是你应该要的答案。

生成器作为迭代器,我们可以用来做什么呢?

  • 使得任何对象都可迭代。只需要写一个生成器函数来串连this(对象集),yielding (反复地产出)它的每次值。然后,使用这个生成器函数来应用对象的[Symbol.iterator]()方法。
  • 简化构建数组的函数。它支持你构建函数,来返回每次循环都可处理的数组,就像这样:
1
2
3
4
5
6
7
8
9
// Divide the one-dimensional array 'icons'
// into arrays of length 'rowLength'.
function splitIntoRows(icons, rowLength) {
  var rows = [];
  for (var i = 0; i < icons.length; i += rowLength) {
     rows.push(icons.slice(i, i + rowLength));
  }
  return rows;
}

生成器可使这代码变为更短些:

1
2
3
4
5
function* splitIntoRows(icons, rowLength) {
  for (var i = 0; i < icons.length; i += rowLength) {
     yield icons.slice(i, i + rowLength);
  }
}

这两者在行为上的唯一不同的地方是,第一种一次性返回处理后的结果,而第二种是返回数组,是一个迭代器,结果可以按要求逐一进行处理。

  • 得到不常见的大小。你不能创建一个无限的数组,但是,你可以返回一个产生无终止序列的生成器。无论调用者想得到多少值,都可以从这生成器中得到。

  • 重构复杂的循环。你是否写过庞大而丑陋的函数?你会不会想要把它打散到两个更为简单的部分中?生成器是你重构工具中的新的一把刀子。当你面对复杂的循环时,你可以将生成数据的那部分代码抽离出来,放到一个分离出来的生成器函数中。然后,循环就可以变为for (var data of myNewGenerator(args))

  • 配合迭代器使用。ES6 并没有为过滤、重组或者直接处理可迭代的数组集 提供额外的库。但是,生成器是搞定这些的好工具,你只需要几行代码就行。

例如,为了让DOM的NodeList支持Array.prototype.filter的等同功能,而不只是数组。部分代码有:

1
2
3
4
5
6
function* filter(test, iterable) {
  for (var item of iterable) {
     if (test(item))
      yield item;
  }
}

所以,生成器很有用,是不是?这是确定的。它们是以一种难以想象的简单方法来实现自定义的迭代器,这个迭代器是ES6中新的数据循环标准。

但是,这些并不是所有生成器可做的作用,这还没有包括它们能做的最为重要的事情。

生成器和异步代码

这里有些我之前写的JS结束代码:

1
2
3
4
5
6
      };
    })
  });
});
  });
});

也许,你要你的代码中也看到类似的事情。异步API 通常需要一个返回函数(callback),这意味着你每次都需要写额外的匿名函数。所以,如果你使用超过三行的代码来做这三件事时,你则正在处理三层的内嵌代码结构。

下面是我写的些JS代码:

1
2
3
4
5
}).on('close', function(){
  done(undefined, undefined);
}).on('error', function(error) {
  done(error);
});

异步API 有单独的错误处理会话,而不是通过 Exception 来处理,不同的API拥有不同的会话。在多数用户的API中,错误通常会被安静地默认过滤掉。在某些,甚至连成功的完成操作也会被默认过滤掉。

到现在,这些问题可以通过异步编程来简单地解决。但是,我们不得不接受,异步代码并不像同步代码看起来简单、好理解。

生成器提供了新的希望而不一定要这方式(异步代码)。

Q.async()是个实验性的方法,利用它有助于生成器通过promise来完成异步代码,其类似于返回型的异步代码。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Synchronous code to make some noise.
function makeNoise() {
  shake();
  rattle();
  roll();
}

// Asynchronous code to make some noise.
// Returns a Promise object that becomes resolved
// when we're done making noise.
function makeNoise_async() {
  return Q.async(function* () {
     yield shake_async();
     yield rattle_async();
     yield roll_async();
  });
}

异步版本的代码主要的区别是必须在每个要调用的异步函数之前添加yield关键字。

如果在Q.async代码中,添加if表达式或者try/catch代码块时,就如普通的异步代码一般。相比于其它异步代码的方式,至少这看起来不会像是学习一种新语言。

如果你理解到现在所介绍的,你可能会喜欢 James Long 的 更为详细的文章A-Study-on-Solving-Callbacks-with-JavaScript-Generators

所以,生成器有助于进行异步编程,其更适合人类的大脑(思维方式),这工作还要继续进行中。在这些工作中,有帮助的可能是更好的语法。A proposal for async functions 同时利用promise和 生成器 来编程,这灵感来源于 C#,此已经在 ES7 的开发日程中。

什么时候我可以用上这些疯狂的特性

在服务端,你在 io.js 中现在就可以用上生成器了,或者 加上--harmony的Node命令行模式中。

对于浏览器来说,目前只有火狐27+和Chrome39+才支持ES6的生成器。现在为了使用生成器,你需要使用 BabelTraceur ,将ES6的代码转为浏览器更为友好的ES5。

值得说明的是:JS中的生成器首先是由 Brendan Eich 实现的,他的设计接近于由 Icon 灵感而发的 Python 生成器。生成器在2006年被火狐2.0有提到,这标准化的路程是比较波折的,在这期间其语法和行为有了不少的变化。在火狐和Chrome中的ES6生成器是由极客 Andy Wingo 实现的,这是由 Bloomberg 支持的。

yield

对于生成器还有更多的东西要说的。我们还没有提到.throw().return()方法,.next()的可选参数,和 yield*语法。但是,我认为这文章已经够长的了,现在足够引起疑惑了。就如生成器一般,我们应该暂停一下,在以后的时间再继续 。

但是下一周,我们要变一下方向。我们这里已经连续说到了两上高级的主题了,难道还没有说ES6会改变你的生活吗?某些是不是有简单又很有用?某些是不是也会让你微笑?ES6 还有些这样的特性。

即将到来的特性会作用于你每天的代码编程,请下周加入我们,一起深入ES6的模板吧。

Comments