20

I was at a node.js meetup today, and someone I met there said that node.js has es6 generators. He said that this is a huge improvement over callback style programming, and would change the node landscape. Iirc, he said something about call stack and exceptions.

I looked them up, but haven't really found any resource that explains them in a beginner-friendly way. What's a high-level overview of generators, and how are the different (or better?) than callbacks?

PS: It'd be really helpful if you could give a snippet of code to highlight the difference in common scenarios (making an http request or a db call).

4

5 回答 5

16

生成器、纤维和协程

“生成器”(除了“生成器”)也是“纤程”或“协程”的基本构建块。使用纤程,您可以“暂停”等待异步调用返回的函数,有效避免“当场”声明回调函数并创建“闭包”。告别回调地狱。

闭包和try-catch

...他说了一些关于调用堆栈和异常的事情

“闭包”的问题在于,即使它们“神奇地”保持回调的局部变量状态,“闭包”也无法保持调用堆栈。

在回调的那一刻,通常调用函数已经返回很久了,因此调用函数上的任何“catch”块都无法捕获异步函数本身或回调中的异常。这提出了一个大问题。因此,您不能将回调+闭包与异常捕获结合起来。

等待

...并会改变节点景观

如果你使用生成器构建一个像Wait.for-ES6(我是作者)这样的 helper lib,你可以完全避免回调和闭包,现在“catch blocks”按预期工作,代码简单明了。

如果您可以提供一段代码来突出常见场景(发出 http 请求或 db 调用)中的差异,那将非常有帮助。

查看Wait.for-ES6示例,查看带有回调和基于生成器的纤维的相同代码。


2021 年更新:所有这些都已被 javascript/ES2020 取代async/await。我的建议是使用 Typescript 和async/await(也基于Promises标准化)

于 2014-02-15T20:36:29.263 回答
10

生成器是即将到来的 ES6中的众多功能之一。所以将来可以在浏览器中使用它们(现在你可以在 FF 中使用它们)。

生成器是迭代器的构造函数。听起来像胡言乱语,所以用更简单的术语来说,它们允许创建对象,以后可以使用.next()方法使用 for 循环之类的东西进行迭代。

生成器的定义方式与函数类似。除了他们有*yield在他们里面。* 是告诉这是生成器,yield类似于return。

例如这是一个生成器:

function *seq(){
    var n = 0;
    while (true) yield n++;
}

然后您可以将此生成器与var s = seq(). 但与函数相比,它不会执行所有操作并为您提供结果,它只会实例化生成器。只有当您运行s.next()生成器时才会执行。这里的yield类似于return,但是当yield运行时,它会暂停生成器并在next之后继续处理下一个表达式。但是当s.next()调用 next 时,生成器将恢复执行。在这种情况下,它将永远继续执行 while 循环。

所以你可以用

for (var i = 0; i < 5; i++){
  console.log( s.next().value )
}

或使用特定的生成器构造:

for (var n of seq()){
    if (n >=5) break;
    console.log(n);
}

这些是关于生成器的基础知识(您可以查看yield*next(with_params)throw()其他附加结构)。请注意,它是关于 ES6 中的生成器(因此您可以在节点和浏览器中完成所有这些操作)。

但是这个无限的数列和回调有什么关系呢?

这里重要的是 yield 会暂停生成器。所以想象你有一个非常奇怪的系统,它以这种方式工作:

您有包含用户的数据库,您需要找到具有某个 ID 的用户的名称,然后您需要在文件系统中检查该用户名称的密钥,然后您需要使用用户的 id 和密钥连接到某个 ftp 和连接后做一些事情。(听起来很荒谬,但我想展示嵌套回调)。

以前你会写这样的东西:

var ID = 1;
database.find({user : ID}, function(userInfo){
    fileSystem.find(userInfo.name, function(key){
        ftp.connect(ID, key, function(o){
            console.log('Finally '+o);
        })
    })
});

哪个是回调里面的回调里面的回调里面的回调。现在您可以编写如下内容:

function *logic(ID){
  var userInfo  = yield database.find({user : ID});
  var key       = yield fileSystem.find(userInfo.name);
  var o         = yield ftp.connect(ID, key);
  console.log('Finally '+o);
}
var s = logic(1);

然后使用它with s.next();如您所见,没有嵌套回调。

因为 node 大量使用嵌套回调,这就是为什么这个家伙告诉生成器可以改变 node 的景观的原因。

于 2014-07-26T02:50:46.753 回答
10

生成器是两个事物的组合 - anIterator和 an Observer

迭代器

迭代器是在调用时返回一个可迭代的东西,这是您可以迭代的东西。从 ES6 开始,所有集合(Array、Map、Set、WeakMap、WeakSet)都符合 Iterable 契约。

生成器(迭代器)是生产者。在迭代中,消费者PULL从生产者那里获得价值。

例子:

function *gen() { yield 5; yield 6; }
let a = gen();

每当您调用a.next()时,您实际上是pull从迭代器中获取值并pause在 处执行yield。下次您调用a.next()时,执行将从先前暂停的状态恢复。

观察者

生成器也是一个观察者,您可以使用它将一些值发送回生成器。用例子更好地解释。

function *gen() {
  document.write('<br>observer:', yield 1);
}
var a = gen();
var i = a.next();
while(!i.done) {
  document.write('<br>iterator:', i.value);
  i = a.next(100);
}

在这里,您可以看到它yield 1的使用方式类似于计算结果为某个值的表达式。它计算的值是作为参数发送给a.next函数调用的值。

因此,第一次i.value将是产生的第一个值 ( 1),当继续迭代到下一个状态时,我们使用 将一个值发送回生成器a.next(100)

你可以在 Node.JS 的什么地方使用它?

生成器广泛用于spawn(来自 taskJS 或 co)函数,其中函数接收生成器并允许我们以同步方式编写异步代码。这并不意味着异步代码被转换为同步代码/同步执行。这意味着我们可以编写看起来像sync但内部仍然是async.

同步被阻塞;异步正在等待。编写阻塞的代码很容易。PULLing 时,赋值位置出现值。PUSHing时,值出现在回调的参数位置

当您使用迭代器时,您PULL将获得来自生产者的价值。当您使用回调时,生产者PUSH将值设置为回调的参数位置。

var i = a.next() // PULL
dosomething(..., v => {...}) // PUSH

在这里,您从回调中提取值,a.next()第二个v => {...}是回调,并将一个值PUSH编入v回调函数的参数位置。

使用这种 pull-push 机制,我们可以像这样编写异步编程,

let delay = t => new Promise(r => setTimeout(r, t));
spawn(function*() {
  // wait for 100 ms and send 1
  let x = yield delay(100).then(() => 1);
  console.log(x); // 1

   // wait for 100 ms and send 2
  let y = yield delay(100).then(() => 2);
  console.log(y); // 2
});

所以,看上面的代码,我们正在编写看起来像它的异步代码blocking(yield 语句等待 100 毫秒然后继续执行),但实际上它是waiting. 生成器的pauseandresume属性使我们能够做到这一点。

它是如何工作的 ?

spawn 函数用于yield promise从生成器中提取承诺状态,等待承诺被解决,然后将解决的值推回生成器,以便它可以使用它。

立即使用

因此,使用生成器和 spawn 函数,您可以清理 NodeJS 中的所有异步代码,使其看起来和感觉上是同步的。这将使调试变得容易。代码看起来也很整洁。

顺便说一句,这是 ES2017 原生的 JavaScript - as async...await. 但是您现在可以使用库中定义的 spawn 函数在 ES2015/ES6 和 ES2016 中使用它们 - taskjs、co 或 bluebird

于 2015-08-16T22:03:50.757 回答
1

概括:

function*定义了一个生成器函数,它返回一个生成器对象。生成器函数的特殊之处在于它在使用()运算符调用时不会执行。而是返回一个迭代器对象。

这个迭代器包含一个next()方法。next()迭代器的方法返回一个对象,该对象包含一个 value 属性,该属性包含产生的值。返回的对象的第二个属性yield是 done 属性,它是 a (如果生成器函数完成boolean,它应该返回)。true

例子:

function* IDgenerator() {
  var index = 0;

  yield index++;
  yield index++;
  yield index++;
  yield index++;
    
}

var gen = IDgenerator(); // generates an iterator object

console.log(gen.next().value); // 0  
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next()); // object, 
console.log(gen.next()); // object done

在这个例子中,我们首先生成一个迭代器对象。在这个迭代器对象上,我们可以调用next()允许我们跳转yieldyield值的方法。我们返回一个同时具有值​​和done属性的对象。

这有什么用?

  • 一些库和框架可能会使用这个构造来等待异步代码的完成,例如redux-saga
  • async await让您等待async事件的新语法在后台使用了它。了解生成器的工作原理将使您更好地了解此构造的工作原理。
于 2018-08-24T17:39:50.777 回答
0

要在 node 中使用 ES6 生成器,您需要安装node >=0.11.2iojs

在节点中,您将需要引用和谐标志:

$ node --harmony app.js 

或者您可以明确引用生成器标志

$ node --harmony_generators app.js

如果你已经安装了 iojs,你可以省略 Harmony 标志。

$ iojs app.js

有关如何使用生成器的高级概述,请查看这篇文章

于 2015-01-29T10:48:00.563 回答