【问题标题】:Understanding Node JS Generators with fs Module使用 fs 模块了解 Node JS 生成器
【发布时间】:2014-04-24 23:42:00
【问题描述】:

一段时间以来,我一直对 Node JS 感到非常兴奋。我最终决定埋头苦干,编写一个测试项目来了解最新的 Harmony 版本的 Node 中的生成器。

这是我非常简单的测试项目:

https://github.com/kirkouimet/project-node

要运行我的测试项目,您可以轻松地从 Github 拉取文件,然后运行它:

node --harmony App.js

这是我的问题 - 我似乎无法让 Node 的异步 fs.readdir 方法与生成器内联运行。其他项目,例如 Galaxysuspend 似乎能够做到。

这是我需要修复的代码块。我希望能够实例化 FileSystem 类型的对象并在其上调用 .list() 方法:

https://github.com/kirkouimet/project-node/blob/4c77294f42da9e078775bb84c763d4c60f21e1cc/FileSystem.js#L7-L11

FileSystem = Class.extend({

    construct: function() {
        this.currentDirectory = null;
    },

    list: function*(path) {
        var list = yield NodeFileSystem.readdir(path);

        return list;
    }

});

我需要提前做一些事情来将 Node 的 fs.readdir 转换为生成器吗?

一个重要的注意事项,我正在解析所有创建的类函数。这让我能够以不同于普通函数的方式处理生成器函数:

https://github.com/kirkouimet/project-node/blob/4c77294f42da9e078775bb84c763d4c60f21e1cc/Class.js#L31-L51

我真的被这个项目难住了。希望得到任何帮助!

这是我想要完成的:

  1. 大量使用类,修改版本的 John Resig 的 JavaScript 类支持继承
  2. 使用生成器获得对 Node 的股票异步调用的内联支持

编辑

我已尝试实现您的示例函数,但遇到了一些麻烦。

list: function*(path) {
    var list = null;

    var whatDoesCoReturn = co(function*() {
        list = yield readdir(path);
        console.log(list); // This shows an array of files (good!)
        return list; // Just my guess that co should get this back, it doesn't
    })();
    console.log(whatDoesCoReturn); // This returns undefined (sad times)

    // I need to use `list` right here

    return list; // This returns as null
}

【问题讨论】:

  • 您可以尝试缩减您的示例并将其包含在问题中吗?您拥有的类逻辑与应用生成器无关,虽然我有建议,但现在很难给出答案。通常,如果代码太大而无法放入您的问题正文中,那么答案对于您以外的任何人都没有帮助。
  • @loganfsmyth 我继续更新我的问题以包含相关代码。谢谢!
  • @KirkOuimet 你的test project url 坏了。

标签: javascript node.js asynchronous generator ecmascript-6


【解决方案1】:

首先,重要的是在您的头脑中建立一个好的模型来准确了解生成器是什么。生成器函数是一个返回生成器对象的函数,当您在其上调用 .next() 时,该生成器对象将逐步执行生成器函数中的 yield 语句。

鉴于该描述,您应该注意到没有提到异步行为。对生成器本身的任何操作都是同步的。您可以立即运行到第一个yield,然后执行setTimeout,然后调用.next() 转到下一个yield,但导致异步行为的是setTimeout,而不是生成器本身。

因此,让我们根据fs.readdir 进行投射。 fs.readdir 是一个异步函数,单独在生成器中使用它不会有任何效果。让我们看看你的例子:

function * read(path){
    return yield fs.readdir(path);
}

var gen = read(path);
// gen is now a generator object.

var first = gen.next();
// This is equivalent to first = fs.readdir(path);
// Which means first === undefined since fs.readdir returns nothing.

var final = gen.next();
// This is equivalent to final = undefined;
// Because you are returning the result of 'yield', and that is the value passed
// into .next(), and you are not passing anything to it.

希望它可以更清楚地说明您仍在同步调用readdir,并且您没有传递任何回调,因此它可能会抛出错误或其他东西。

那么如何从生成器中获得良好的行为呢?

通常这是通过让生成器在实际计算值之前产生一个表示 readdir 的结果的特殊对象来实现的。

对于(不切实际的)示例,yield一个函数是一种产生表示值的东西的简单方法。

function * read(path){
    return yield function(callback){
        fs.readdir(path, callback);
    };
}

var gen = read(path);
// gen is now a generator object.

var first = gen.next();
// This is equivalent to first = function(callback){ ... };

// Trigger the callback to calculate the value here.
first(function(err, dir){
  var dirData = gen.next(dir);
  // This will just return 'dir' since we are directly returning the yielded value.

  // Do whatever.
});

确实,您会希望这种类型的逻辑继续调用生成器,直到完成所有 yield 调用,而不是对每个调用进行硬编码。不过要注意的主要一点是,现在生成器本身看起来是同步的,read 函数之外的所有内容都是超级通用的。

您需要某种生成器包装函数来处理这个产值过程,而您的 suspend 示例正是这样做的。另一个例子是co

“返回代表值的东西”方法的标准方法是返回 promisethunk,因为像我一样返回函数有点丑。

使用 thunkco 库,您可以在没有示例函数的情况下执行上述操作:

var thunkify = require('thunkify');
var co = require('co');
var fs = require('fs');
var readdir = thunkify(fs.readdir);

co(function * (){
    // `readdir` will call the node function, and return a thunk representing the
    // directory, which is then `yield`ed to `co`, which will wait for the data
    // to be ready, and then it will start the generator again, passing the value
    // as the result of the `yield`.
    var dirData = yield readdir(path, callback);

    // Do whatever.
})(function(err, result){
    // This callback is called once the synchronous-looking generator has returned.
    // or thrown an exception.
});

更新

您的更新仍然有些混乱。如果您希望您的 list 函数成为生成器,那么无论您在哪里调用它,都需要使用 co outside of listco 内部的所有内容都应该基于生成器,co 之外的所有内容都应该基于回调。 co 不会使 list 自动异步。 co 用于将基于生成器的异步流控制转换为基于回调的流控制。

例如

list: function(path, callback){
    co(function * (){
      var list = yield readdir(path);

      // Use `list` right here.

      return list;
    })(function(err, result){
      // err here would be set if your 'readdir' call had an error
      // result is the return value from 'co', so it would be 'list'.

      callback(err, result);
    })
}

【讨论】:

  • // This is equivalent to first = fs.readdir(path); 我想这里也是undefined,因为fs.readdir(path)会在屈服之前被评估。
  • @thefourtheye 好电话,我添加了一条注释。我试图使映射到他的示例显而易见,但我应该提到这一点。
  • 嗨@loganfsmyth - 感谢您花时间帮助我!我试图实现您的示例功能,但遇到了一些麻烦。你能看一下并给我一些反馈吗? github.com/kirkouimet/project-node/blob/master/…
  • @Kirk 更新了我的答案。请在问题中发布代码,不赞成链接到外部位置。
  • 关于@Kirk 问什么应该是适当的数据结构来处理生成器输出? for-of 理解好,所以我们传递迭代器而不是数组?
【解决方案2】:

@loganfsmyth 已经为您的问题提供了a great answer。我回答的目的是帮助您了解 JavaScript 生成器的实际工作原理,因为这是正确使用它们的非常重要的一步。

生成器实现state machine,这个概念本身并不新鲜。新的是,生成器允许使用熟悉的 JavaScript 语言结构(例如,foriftry/catch)来实现状态机,而不会放弃线性代码流。

生成器最初的目标是生成一个数据序列,这与异步无关。示例:

// with generator

function* sequence()
{
    var i = 0;
    while (i < 10)
        yield ++i * 2;
}

for (var j of sequence())
    console.log(j);

// without generator

function bulkySequence()
{
    var i = 0;
    var nextStep = function() {
        if ( i >= 10 )
            return { value: undefined, done: true };
        return { value: ++i * 2, done: false };
    }
    return { next: nextStep };
}

for (var j of bulkySequence())
    console.log(j);

第二部分 (bulkySequence) 展示了如何在不使用生成器的情况下以传统方式实现相同的状态机。在这种情况下,我们不再能够使用while 循环来生成值,并且继续通过nextStep 回调发生。此代码体积庞大且难以阅读。

让我们引入异步。 在这种情况下,状态机的下一步将不是由for of 循环驱动,而是由一些外部事件驱动。我将使用计时器间隔作为事件的来源,但它也可以是 Node.js 操作完成回调,或 Promise 解析回调。

这个想法是在不使用任何外部库的情况下展示它是如何工作的(如QBluebirdCo 等)。没有什么能阻止发电机自动驾驶到下一步,这就是以下代码的作用。一旦异步逻辑的所有步骤都完成(10 个计时器滴答声),doneCallback 将被调用。请注意,我不会在此处使用yield 返回任何有意义的数据。我只是用它来暂停和恢复执行:

function workAsync(doneCallback)
{
    var worker = (function* () {
        // the timer callback drivers to the next step
        var interval = setInterval(function() { 
            worker.next(); }, 500);

        try {
            var tick = 0;
            while (tick < 10 ) {
                // resume upon next tick
                yield null;
                console.log("tick: " + tick++);
            }
            doneCallback(null, null);
        }
        catch (ex) {
            doneCallback(ex, null);
        }
        finally {
            clearInterval(interval);
        }
    })();

    // initial step
    worker.next();
}

workAsync(function(err, result) { 
    console.log("Done, any errror: " + err); });

最后,让我们创建一个事件序列:

function workAsync(doneCallback)
{
    var worker = (function* () {
        // the timer callback drivers to the next step
        setTimeout(function() { 
            worker.next(); }, 1000);

        yield null;
        console.log("timer1 fired.");

        setTimeout(function() { 
            worker.next(); }, 2000);

        yield null;
        console.log("timer2 fired.");

        setTimeout(function() { 
            worker.next(); }, 3000);

        yield null;
        console.log("timer3 fired.");

        doneCallback(null, null);
    })();

    // initial step
    worker.next();
}

workAsync(function(err, result) { 
    console.log("Done, any errror: " + err); });

一旦您理解了这个概念,您就可以继续使用 Promise 作为生成器的包装器,这会将其提升到一个更强大的水平。

【讨论】:

  • 不错。我认为我们(尝试)答案的长度很清楚生成器很难解释和理解。 :)
猜你喜欢
  • 2015-01-17
  • 2018-01-05
  • 1970-01-01
  • 1970-01-01
  • 2017-04-15
  • 2019-07-03
  • 1970-01-01
  • 2014-04-23
  • 2021-08-15
相关资源
最近更新 更多