ES6 Generator 基础指南

本文翻译自:The Basics Of ES6 Generatorsjavascript

因为我的能力有限,翻译中不免有纰漏和错误,望不吝指正issuejava

JavaScript ES6(译者注:ECMAScript 2015)中最使人兴奋的特性之一莫过于Generator函数,它是一种全新的函数类型。它的名字有些奇怪,初见其功能时甚至更会有些陌生。本篇文章旨在解释其基本工做原理,并帮助你理解为何Generator将在将来JS中发挥强大做用。node

Generator从运行到完成的工做方式

但咱们谈论Generator函数时,咱们首先应该注意到的是,从“运行到完成”其和普通的函数表现有什么不一样之处。git

不论你是否已经意识到,你已经潜意识得认为函数具备一些很是基础的特性:函数一旦开始执行,那么在其结束以前,不会执行其余JavaScript代码。es6

例如:github

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循环会执行至关长的时间,长于1秒钟,可是在foo()函数执行的过程当中,咱们带有console.log(...)的定时器并不可以中断foo()函数的运行。所以代码被阻塞,定时器被推入事件循环的最后,耐心等待foo函数执行完成。编程

假若foo()能够被中断执行?它不会给咱们的带来史无前例的浩劫吗?设计模式

函数能够被中断对于多线程编程来讲确实是一个挑战,可是值得庆幸的是,在JavaScript的世界中咱们不必为此而担忧,由于JS老是单线程的(在任什么时候间只有一条命令/函数被执行)。数组

注意: Web Workers是JavaScript中实现与JS主线程分离的独立线程机制,总的说来,Web Workers是与JS主线程平行的另一个线程。在这儿咱们并不介绍多线程并发的一个缘由是,主线程和Web Workers线程只可以经过异步事件进行通讯,所以每一个线程内部从运行到结束依然遵循一个接一个的事件循环机制。浏览器

运行-中止-运行

因为ES6Generators的到来,咱们拥有了另一种类型的函数,这种函数能够在执行的过程当中暂停一次或屡次,在未来的某个时间继续执行,而且容许在Generator函数暂停的过程当中运行其余代码。

若是你曾经阅读过关于并发或者多线程编程的资料,那你必定熟悉“协程”这一律念,“协程”的意思就是一个进程(就是一个函数)其能够自行选择终止运行,以即可以和其余代码“协做”完成一些功能。这一律念和“preemptive”相对,preemptive认为能够在进程/函数外部对其终止运行。

根据ES6 Generator函数的并发行为,咱们能够认为其是一种“协程”。在Generator函数体内部,你可使用yield关键字在函数内部暂停函数的执行,在Generator函数外部是没法暂停一个Generator函数执行的;每当Generator函数遇到一个yield关键字就将暂停执行。

而后,一旦一个Generator函数经过yield暂停执行,其不可以自行恢复执行,须要经过外部的控制来从新启动generator函数,咱们将在文章后面部分介绍这是怎么发生的。

基本上,只要你愿意,一个Generator函数能够暂停执行/从新启动任意屡次。实际上,你能够再Generator函数内部使用无限循环(好比非著名的while (true) { .. })来使得函数能够无尽的暂停/从新启动。而后这在普通的JS程序中倒是疯狂的行径,甚至会抛出错误。可是Generator函数却可以表现的很是明智,有些时候你确实想利用Generator函数这种无尽机制。

更为重要的是,暂停/从新启动不只仅用于控制Generator函数执行,它也能够在generator函数内部和外部进行双向的通讯。在普通的JavaScript函数中,你能够经过传参的形式将数据传入函数内容,在函数内部经过return语句将函数的返回值传递到函数外部。在generator函数中,咱们经过yield表达式将信息传递到外部,而后经过每次重启generator函数将其余信息传递给generator。

Generator 函数的语法

然咱们看看新奇而且使人兴奋的generator函数的语法是怎样书写的。

首先,新的函数声明语法:

function *foo() {
    // ..
}

发现*符号没?显得有些陌生且有些奇怪。对于从其余语言转向JavaScript的人来讲,它看起来很像函数返回值指针。可是不要被迷惑到了,*只是用于标识generator函数而已。

你可能会在其余的文章/文档中看到以下形式书写generator函数function* foo(){},而不是这样function *foo() {}(*号的位置有所不一样)。其实两种形式都是合法的,可是最近我认为后面一种形式更为准确,所以在本篇文章中都是使用后面一种形式。

如今,让咱们来讨论下generator函数的内部构成吧。在不少方面,generator函数和普通函数无异,只有在generator函数内部有一些新的语法。

正如上面已经说起,咱们最早须要了解的就是yield关键字,yield__被视为“yield表达式”(并非一条语句),由于当咱们从新启动generator函数的时候,咱们能够传递信息到generator函数内部,不论咱们传递什么进去,都将被视为yield__表达式的运行结果。

例如:

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

yield "foo"表达式会在generator函数暂停时把“foo”字符串传递到外部。同时,当generator函数恢复执行的时候,其余的值又会经过其余表达式传入到函数里面做为yield表达式的返回值加1最后再将结果赋值给x变量。

看到generator函数的双向通讯了吗?generator函数将‘’foo‘’字符串传递到外部,暂停函数执行,在未来的某个时间点(多是当即也多是很长一段时间后),generator会被重启,而且会传递一个值给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 迭代器

“Generator 迭代器”,是否是至关晦涩难懂?

迭代器是一种特殊的行为,准确说是一种设计模式,当咱们经过调用next()方法去遍历一组值的集合时,例如,咱们经过在长度为5的数组[1, 2, 3, 4, 5]上面实现了迭代器。当咱们第一次调用next()的时候,会返回1。第二次调用next()返回2,如此下去,当全部的值都返回后,再次调用next()将返回null或者false或其余值,这意味着你已经遍历完真个数组中的值了。

咱们是经过和generator迭代器进行交互来在generator函数外部控制generator函数,这听起来比起实际上有些复杂,考虑下面这个愚蠢的(简单的)例子:

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

为了遍历*foo()generator函数中的全部值,咱们首先须要构建一个迭代器,咱们怎么去构建这个迭代器呢?很是简单!

var it = foo();

如此之简单,咱们仅仅想执行普通函数同样执行generator函数,其将返回一个迭代器,可是generator函数中的代码并不会运行。

这彷佛有些奇怪,而且增长了你的理解难度。你甚至会停下来思考,问为何不经过var it = new foo()的形式来执行generator函数呢,这语法后面的缘由可能至关复杂并超出了咱们的讨论范畴。

好的,如今让咱们开始迭代咱们的generator函数,以下:

var message = it.next();

经过上面的语句,yield表达式将1返回到函数外部,可是返回的值可能比想象中会多一些。

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

在每一调用next()后,咱们实际上从yield表达式的返回值中获取到了一个对象,这个对象中有value字段,就是yield返回的值,同时还有一个布尔类型的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字段里面呢?

...

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 }

... 不是.

依赖于generator函数的最终返回值也许并非一个最佳实践,由于当咱们经过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()` here
console.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)给generator函数,就像普通函数同样,是的参数x5.

在第一次执行next(..)的时候,咱们并无传递任何值,为何?由于在generator内部并无yield表达式来接收咱们传递的值。

假如咱们真的在第一次调用next(..)的时候传递了值进去,也不会带来什么坏处,它只是将这个传入的值抛弃而已。ES6代表,generator函数在这种状况只是忽略了这些没有被用到的值。(注意:在写这篇文章的时候,Chrome和FF的每夜版支持这一特性,可是其余浏览有可能没有彻底支持这一特性甚至可能会抛出错误)(译者注:文章发布于2014年)

yield(x + 1)表达式将传递值6到外部,在第二次调用next(12)时候,传递12到generator函数内部做为yield(x + 1)表达式的值,所以y被赋值为12 * 2,值为24。接下来,下一条yield(y / 3)(yield (24 / 3))将向外传递值8。第三次调用next(13)传递13到generator函数内部,给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 5

console.log( v ); // still `5`, not `6` :(

正如你所见,经过调用foo()生成的迭代器经过for..of循环来迭代,循环自动帮你对迭代器进行遍历迭代,每次迭代返回一个值,直到done: true,只要done: false,每次循环都将从value属性上获取到值赋值给迭代的变量(例子中的v)。一旦当donetrue。循环迭代结束。(for..of循环不会对generator函数最终的return值进行处理)

正如你所看到的,for..of循环忽略了generator最后的return 6的值,同时,循环没有暴露next()出来,所以咱们也不可以向generator函数内传递数据。

总结

OK,上面是关于generator函数的基本用法,若是你依然对generator函数感到费解,不要担忧,咱们全部人在一开始感受都是那样的。

咱们很天然的想到这一外来的语法对咱们实际代码有什么做用呢?generator函数有不少做用,咱们只是挖掘了其很是粗浅的一部分。在咱们发现generator函数如此强大以前咱们应该更加深刻的了解它。

在你练习上面代码片断以后(在Chrome或者FF每夜版本,或者0.11+带有--harmony的node环境下),下面的问题也许会浮出水面:(译者注:现代浏览器最新版本都已支持Generator函数)

  1. 怎样处理generator内部错误?

  2. 在generator函数内部怎么调用其余generator函数?

  3. 异步代码怎么和generator函数协同工做?

这些问题,或者其余的问题都将在随后的文章中覆盖,敬请期待。

相关文章
相关标签/搜索