Generator 函数基础(一) (The Basics of ES6 Generators)

来源:互联网 发布:淘宝平板电脑排行榜 编辑:程序博客网 时间:2024/06/14 11:23

原文地址:The Basics Of ES6 Generators
作者简介:Kyle Simpson is an Open Web Evangelist from Austin, TX, passionate about all things JavaScript. He's an author, workshop trainer, tech speaker, and OSS contributor/leader.

JavaScript ES6 最令人兴奋的新特性中有一个是一种新类型函数,叫作 generator。名字有点奇怪,但是第一次看它的行为表现时会感到更奇怪的。本文旨在解释 generator 函数如何工作的基础知识,并帮助你理解为什么它们对于 JS 的未来会如此强大。

Run-To-Completion

在我们讨论 generators 函数时,首先要注意的是他们 “从执行到完成” 所期望的结果与标准的函数有什么不同。

无论你是否意识到,你总是会为你的函数假设一些基础的功能:当函数开始执行时,它总是走从执行到完成这个过程,在此之前,其它的 JS 代码是无法运行的。

栗子:

setTimeout(function(){    console.log("Hello World");},1);function foo() {    // NOTE: don't ever do crazy long-running loops like this    for (var i=0; i<=1E10; i++) {        console.log(i);    }}foo();// 0..1E10// "Hello World"

在这里, for 循环将会执行很长时间,肯定超过了 1ms ,但是我们使用定时器回调执行console.log(.. ) 时,是无法打断 for 循环的,所以它被卡在了执行栈(事件轮询)的后面,并且耐心等待被翻牌子(_)。

如果 foo() 可以中断呢?这不会对我们的程序造成破坏吗?

这正是多线程编程的挑战,但是,非常幸运,我们在 JavaScript 环境中不必担心这样的事情,以为 JS 总是单线程的(在给定的时间只有一个命令/函数在执行)。

Note: Web Workers 是一种机制,利用这种机制你可以为 JS 程序的一部分独立出一个完整的线程,它完全与 JS 程序的主线程并行运行。这样操作并不会将多线程的问题引入到我们的程序中,因为两个线程只能通过正常的异步事件相互通信,而且这些异步事件都会遵循 one-at-a-time 行为准则——执行到完成。

Run...Stop...Run

有了 ES6 generators ,我们就有了一种不同类型的函数,它们可以在执行的过程中暂停一次或者多次,并且之后还可以恢复,在这些暂停期间还可以运行其它代码。

如果你读过任何关于并发或者线程编程的东西,你可能已经看到了 cooperative(协作式) 一词,这基本上表明一个过程(在我们的例子中是一个函数)本身就会选择什么时候允许中断,这样它就可以配合其它代码了。这个概念与 preemptive(抢占式) 形成对比,这表明 过程/函数 可能会违背其意愿。

ES6 generator 函数的并发行为是 cooperative(协作式)。在 generator 函数体内部,你可以使用新的 yield 关键字从内部暂停函数。在 generator 函数外部是无法暂停它的。当 generator 函数遇到 yield 关键字时,就回家暂停。

然而,一旦 generator 暂停,它就不能自行恢复了。必须使用外部控制来重启 generator。我们一会将会解释这种情况。

因此,基本上,generator 函数可以根据你的需要停止和重新启动任意多次。事实上,你可以使用无限循环(如臭名昭著的 while(true){})指定一个 generator 函数,这个循环本质上是不会结束的。在 JS 程序中,这样做通常是疯狂的或者错误的,但是使用 generator 函数是完全理智的,有时候甚至是你想要做的。

更重要的是,这种停止和启动不仅仅是对 generator 函数执行的控制,而且在它进行的过程中,还使得 2-way(双向)消息传进和传出 generator 函数。使用正常的函数,你会在函数起始处获取参数,然后在最后获取返回值。使用 generator 函数,你可以在每个 yield 暂停的时候传递出消息,然后在每次重新启动的时候传进去消息。

Syntax Please!

现在让我们来讨论令人兴奋的 generator 函数的语法。
首先,新的声明语法:

function *foo() {    // ..}

看到那个 * 了吗?看起来有点怪异。对于那些学习其它语言的人来说,它看起来很槽糕,就像一个函数返回指针类型一样。但是不要被迷惑了!这只是标识 generator 函数是一种特殊的函数类型而已。

你可能已经看过其它的文章/文档,它们可能使用 function* foo(){} 来代替 function *foo(){}(仅仅是 * 号的位置不同)。两者都是有效的,但我最近决定支持 function *foo(){} 这种写法,因为它更准确,所以这里我使用的这种写法。
现在,让我们来讨论一下 generator 函数的函数体。 Generator 函数在大多数方面和普通函数是一样的。在 generator 函数内部只有很少的新语法需要学习。

如上所述,我们主要的新玩具就是 yield 关键字。yield__ 被称为 “ yield 表达式”(而不是一个语句),因为当我们重启 generator 时,我们将会传入一个值,并且我们传入的值将是改yield__ 表达式的计算结果。

栗子:

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

当 generator 函数在 yield 关键字处暂停时,yield "foo" 表达式将会把 "foo" 字符串传递出来,并且无论何时(如果有)generator 函数重启时,传递进去的任何值都将是该 yield表达式的结果,然后加 1 ,赋值给变量 x。

看到双向通信了吗?你将 “foo” 字符串传递出来,暂停函数,稍后(可能会立即,可能会很长时间)generator 重启,并将会传递进来一个值。几乎就像 yield 关键字做了一个值得请求一样。

在任何表达式的位置,你可以在表达式/语句中单独使用 yield,而且假定 undefined 将在yield 结束时返回。所以:


// note: `foo(..)` here is NOT a generator!!function foo(x) {    console.log("x: " + x);}function *bar() {    yield; // just pause    foo( yield ); // pause waiting for a parameter to pass into `foo(..)`}

Generator Iterator

"Generator Iterator"。相当的绕口,有没有?

迭代器是一种特殊的行为,实际上它是一种设计模式,我们通过 next() 来遍历一组有序的值。想象一下,例如在其中有五个值的数组上使用迭代器:[1, 2, 3, 4, 5]。第一个 next() 调用将返回 1,第二个 next() 调用将返回 2,以此类推。在返回所有值之后,next() 将返回 null 或 false,或以其他方式向您发出信号,表示你已经迭代了数据容器中的所有值。

我们从外部控制 generator 函数方式是构造一个 generator 的迭代器并与其交互。这听起来貌似比它实际的原理更复杂。让我们来看看下面这个简单的例子:

function *foo() {    yield 1;    yield 2;    yield 3;    yield 4;    yield 5;}

要遍历 generator 函数 *foo() 的值,我们需要构造一个迭代器。我该如何做呢?简单!

var it = foo();

哦!因此,以正常方式调用 generator 函数实际上并不会执行它的任何内容。

这可能会让你感觉有点绕。你也可能会想,为什么它不是 var it = new foo()。怎么说,语法背后的问题是很复杂的,这个问题已经超出了我们的讨论范围了。

那么现在,为了迭代我们的 generator 函数,我们只需要做:

var message = it.next();

 yield 1 处,我们会得到返回值 1,但是这并不是我们得到的唯一值。

console.log(message); // { value:1, done:false }

我们实际上从每个 next() 调用中会获取到一个对象,该对象对于 yielded-out 的值有一个 value 属性,而 done 属性是一个布尔值,表示 generator 函数是否完全完成。

console.log( it.next() ); // { value:2, done:false }console.log( it.next() ); // { value:3, done:false }console.log( it.next() ); // { value:4, done:false }console.log( it.next() ); // { value:5, done:false }

有趣的是,当我们得到值 5 时,done 的值仍然是 false。因为从技术上来看,generator 函数还没有完成。我们仍然可以调用最后一个 next(),如果我们传入一个值,它将必须被设置为yield 5 表达式的结果。只有这样,generator 函数才算执行完成。

所以现在:

console.log( it.next() ); // { value:undefined, done:true }

所以,我们的 generator 函数最终结果是我们完成了这个函数,但没有得到结果(因为我们已经耗尽了所有的 yield__ 语句)。

你可能会这样想,在 generator 函数中我是否可以使用 return 返回值呢?如果我这样做了,那么该值将会被设置为 value 属性的值吗?

Yes ...

function *foo() {    yield 1;    return 2;}var it = foo();console.log( it.next() ); // { value:1, done:false }console.log( it.next() ); // { value:2, done:true }

... and no.

在 generator 中依靠 return 返回值可能不是一个好主意,因为当使用 for ... of 循环迭代 generator 函数时,最终的 return 返回值将会被抛弃。

为了完整起见,我们来看一看在我们迭代 generator 函数时是如何传入和传出消息的。

function *foo(x) {    var y = 2 * (yield (x + 1));    var z = yield (y / 3);    return (x + y + z);}var it = foo( 5 );// note: not sending anything into `next()` hereconsole.log( it.next() );       // { value:6, done:false }console.log( it.next( 12 ) );   // { value:8, done:false }console.log( it.next( 13 ) );   // { value:42, done:true }

你可以看到,在我们初始化迭代器实例时,仍然可以使用 foo(5) 传递参数(在我们的示例中为 x),就是正常函数一样,使 x 的值为 5。

第一次 next() 调用,我们可以不传入任何值。为什么?因为没有 yield 表达式接收我们传入的东西。

但是,如果我们确实在第一次调用 next() 时传入了值,不会发生什么不好的事情的。这只是一个将会被抛弃的值。ES6 表示在这种情况下, generator 函数会忽略无用的值的。(注意:在撰写本文时,Chrome 和 FF 浏览器都很好,但其他浏览器可能尚未完全符合标准,并且在这种情况下可能会错误地抛出错误)。

yield(x + 1) 将会返回值 6。第二次调用 next(12) 将会传入 12 给正在等待的 yield(x + 1) 表达式,因此 y 就会被设置为 12 * 2,值为 24。然后 yield(y / 3) (yield(24 / 3))将会返回值 8。第三次调用 next(13) 会传入 13 给正在等待的 yield(y / 3) 表达式,z 被赋值为 13。

最终,返回 return (x + y + z) 就是 return(5 + 24 + 13),42 就是最后的返回值。

多读几次,第一次看到它多少会感到有些奇怪的。

for .. of

ES6 在语法级别包含了迭代器模式,for .. of 循环对运行迭代器提供了直接的支持。

栗子:

function *foo() {    yield 1;    yield 2;    yield 3;    yield 4;    yield 5;    return 6;}for (var v of foo()) {    console.log( v );}// 1 2 3 4 5console.log( v ); // still `5`, not `6` :(

如你所见,由 foo() 创建的迭代器由 foo .. of 循环自动捕获,并为每个值自动迭代一次,直到 done: true。只要 done: false,它将自动提取 value 属性并将其赋值给你的迭代变量(在本例中为 v)。一旦 done 为 true,则循环迭代停止(并且不返回任何最终值,如果有的话)。

如上所述,你可以看到 for .. of 循环忽略并抛弃了 return 6。此外,由于没有暴露的next() 调用,因此在需要将值传递给 generator 函数的情况下,for .. of 循环不能用于上述操作。

Summary

好的,这就是 generator 函数的基础知识。如果仍然觉得有些令人费解,不要担心,刚开始的时候我们都这样。

很自然的想知道这个新的外来玩具将会为你的代码做些什么。不过,它们还有更多的东西等待我们的探索,我们只是刚刚触摸到皮毛。所以我们必须更加的深入,才能发现它们能够/将会有多么的强大。

在运行上述代码片段(试着用 Chrome 或者 FF 又或者 node 0.11+ with --harmony)之后,可能会出现以下问题:

  1. 错误处理如何工作?
  2. 一个 generator 函数如何调用另一个?
  3. 异步编码如何和 generator 函数配合使用?

等等这些问题,将在这一系列后面的文章中结束,敬请期待!

原创粉丝点击