Понимание Node Генераторы JS с модулем fs

Я очень волнуюсь о Node JS на некоторое время. Я, наконец, решил свалиться и написать тестовый проект, чтобы узнать о генераторах в последней версии Harmony Node.

Вот мой очень простой тестовый проект:

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

Чтобы запустить мой тестовый проект, вы можете легко вытащить файлы из Github и запустить его с помощью:

node --harmony App.js

Здесь моя проблема - я не могу получить метод асинхронного fs.readdir Node для запуска встроенных генераторов. Другие проекты, такие как Galaxy и suspend, похоже, могут это сделать.

Вот блок кода, который мне нужно исправить. Я хочу иметь возможность создать экземпляр объекта типа 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

Я действительно был в тупике с этим проектом. Любить любую помощь!

Вот что я пытаюсь выполнить:

  • Тяжелое использование классов с модифицированной версией поддержки JavaScript JavaScript JavaScript с наследованием
  • Использование генераторов для поддержки inline-поддержки для анимированных вызовов 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
}

Ответ 1

Прежде всего важно иметь в голове хорошую модель того, что такое генератор. Функция-генератор - это функция, возвращающая объект-генератор, и этот объект-генератор будет проходить через операторы yield внутри функции-генератора, как вы вызываете .next() на нем.

Учитывая это описание, вы должны заметить, что асинхронное поведение не упоминается. Любое действие на генераторе само по себе является синхронным. Вы можете сразу перейти к первому 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.

Стандартный метод для метода "вернуть что-то представляющее значение" - это вернуть promise или thunk, так как возвращение функции, как я, было довольно уродливо.

В библиотеках thunk и co вы можете сделать это без функции примера:

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

Update

У вашего обновления все еще есть путаница. Если вы хотите, чтобы ваша функция 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);
    })
}

Ответ 2

@loganfsmyth уже предоставляет отличный ответ на ваш вопрос. Цель моего ответа - помочь вам понять, как работают генераторы JavaScript, поскольку это очень важный шаг для правильного их использования.

Генераторы реализуют конечный автомат, концепция, которая не является чем-то новым сама по себе. Что нового в том, что генераторы позволяют использовать привычную конструкцию языка JavaScript (например, for, if, try/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 или обратным вызовом разрешения обещания.

Идея состоит в том, чтобы показать, как это работает без использования каких-либо внешних библиотек (например, Q, Bluebird, Co и т.д.). Ничто не останавливает генератор от самостоятельного вождения до следующего шага и того, что делает следующий код. После завершения всех шагов асинхронной логики (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 в качестве оберток для генераторов, который переводит его на следующий мощный уровень.