了解带有fs模块的节点JS生成器

时间:2014-03-19 00:21:48

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

我对Node JS感到非常兴奋。我最终决定关注并编写一个测试项目,以了解最新的Harmony构建节点中的生成器。

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

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的库存异步调用的内联支持
  3. 修改

    我试图实现你的示例函数,但我遇到了一些麻烦。

    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
    }
    

2 个答案:

答案 0 :(得分:17)

首先,重要的是要有一个好的模型在你的头脑中,确切地说是发电机。生成器函数是一个返回生成器对象的函数,当您在其上调用yield时,该生成器对象将逐步执行生成器函数中的.next()语句。

鉴于该描述,您应该注意到未提及异步行为。对发电机本身的任何动作都是同步的。您可以立即转到第一个yield,然后执行setTimeout,然后致电.next()转到下一个yield,但setTimeoutfs.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次调用,而不是对每次调用进行硬编码。不过要注意的是,现在生成器本身看起来是同步的,thunk函数之外的所有东西都是超级通用的。

您需要某种处理此屈服值过程的生成器包装函数,而suspend的示例正是如此。另一个例子是co

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

使用covar 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函数成为生成器,那么无论您在何处调用它,都需要使用list之外的co 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); }) } 用于将基于生成器的异步流控制转换为基于回调的流控制。

e.g。

{{1}}

答案 1 :(得分:9)

@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); });

一旦理解了这个概念,就可以继续使用promises作为生成器的包装器,从而将它带到下一个强大的层次。