Как избежать длительного вложения асинхронных функций в Node.js - PullRequest
156 голосов
/ 20 ноября 2010

Я хочу создать страницу, которая отображает некоторые данные из БД, поэтому я создал несколько функций, которые получают эти данные из моей БД.Я просто новичок в Node.js, так что, насколько я понимаю, если я захочу использовать их все на одной странице (HTTP-ответ), мне придется их всех вкладывать:1003 * Если таких функций много, то вложение становится проблемой .

Есть ли способ избежать этого?Я предполагаю, что это связано с тем, как вы комбинируете несколько асинхронных функций, что кажется фундаментальным.

Ответы [ 23 ]

73 голосов
/ 20 ноября 2010

Интересное наблюдение.Обратите внимание, что в JavaScript вы обычно можете заменить встроенные анонимные функции обратного вызова на именованные переменные функции.

Следующее:

http.createServer(function (req, res) {
   // inline callback function ...

   getSomeData(client, function (someData) {
      // another inline callback function ...

      getMoreData(client, function(moreData) {
         // one more inline callback function ...
      });
   });

   // etc ...
});

Может быть переписано, чтобы выглядеть примерно так:

var moreDataParser = function (moreData) {
   // date parsing logic
};

var someDataParser = function (someData) {
   // some data parsing logic

   getMoreData(client, moreDataParser);
};

var createServerCallback = function (req, res) {
   // create server logic

   getSomeData(client, someDataParser);

   // etc ...
};

http.createServer(createServerCallback);

Однако, если вы не планируете повторно использовать логику обратного вызова в других местах, часто намного проще читать встроенные анонимные функции, как в вашем примере.Это также избавит вас от необходимости искать имя для всех обратных вызовов.

Кроме того, обратите внимание, что как @ pst отмечено в комментарии ниже, если вы обращаетесь к переменным замыкания внутри внутреннегофункции, выше не будет простым переводом.В таких случаях использование встроенных анонимных функций еще более предпочтительно.

63 голосов
/ 27 ноября 2010

Кей, просто используйте один из этих модулей.

Получится так:

dbGet('userIdOf:bobvance', function(userId) {
    dbSet('user:' + userId + ':email', 'bobvance@potato.egg', function() {
        dbSet('user:' + userId + ':firstName', 'Bob', function() {
            dbSet('user:' + userId + ':lastName', 'Vance', function() {
                okWeAreDone();
            });
        });
    });
});

В это:

flow.exec(
    function() {
        dbGet('userIdOf:bobvance', this);

    },function(userId) {
        dbSet('user:' + userId + ':email', 'bobvance@potato.egg', this.MULTI());
        dbSet('user:' + userId + ':firstName', 'Bob', this.MULTI());
        dbSet('user:' + userId + ':lastName', 'Vance', this.MULTI());

    },function() {
        okWeAreDone()
    }
);
18 голосов
/ 21 ноября 2010

По большей части я бы согласился с Даниэлем Вассалло. Если вы можете разбить сложную и глубоко вложенную функцию на отдельные именованные функции, то обычно это хорошая идея. В тех случаях, когда имеет смысл делать это внутри одной функции, вы можете использовать одну из множества доступных асинхронных библиотек node.js. Люди придумали множество разных способов решения этой проблемы, поэтому взгляните на страницу модулей node.js и посмотрите, что вы думаете.

Я сам написал для этого модуль, который называется async.js . Используя это, приведенный выше пример может быть обновлен до:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  async.series({
    someData: async.apply(getSomeDate, client),
    someOtherData: async.apply(getSomeOtherDate, client),
    moreData: async.apply(getMoreData, client)
  },
  function (err, results) {
    var html = "<h1>Demo page</h1>";
    html += "<p>" + results.someData + "</p>";
    html += "<p>" + results.someOtherData + "</p>";
    html += "<p>" + results.moreData + "</p>";
    res.write(html);
    res.end();
  });
});

Одна приятная особенность этого подхода заключается в том, что вы можете быстро изменить свой код для параллельного извлечения данных, изменив функцию 'series' на 'parallel'. Более того, async.js будет также работают в браузере, поэтому вы можете использовать те же методы, что и в файле node.js, если столкнетесь с хитрым асинхронным кодом.

Надеюсь, это полезно!

18 голосов
/ 03 декабря 2012

Вы можете использовать этот трюк с массивом, а не с вложенными функциями или модулем.

Намного легче на глазах.

var fs = require("fs");
var chain = [
    function() { 
        console.log("step1");
        fs.stat("f1.js",chain.shift());
    },
    function(err, stats) {
        console.log("step2");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step3");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step4");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step5");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("done");
    },
];
chain.shift()();

Вы можете расширить идиому для параллельных процессов или даже параллельных цепочек процессов:

var fs = require("fs");
var fork1 = 2, fork2 = 2, chain = [
    function() { 
        console.log("step1");
        fs.stat("f1.js",chain.shift());
    },
    function(err, stats) {
        console.log("step2");
        var next = chain.shift();
        fs.stat("f2a.js",next);
        fs.stat("f2b.js",next);
    },
    function(err, stats) {
        if ( --fork1 )
            return;
        console.log("step3");
        var next = chain.shift();

        var chain1 = [
            function() { 
                console.log("step4aa");
                fs.stat("f1.js",chain1.shift());
            },
            function(err, stats) { 
                console.log("step4ab");
                fs.stat("f1ab.js",next);
            },
        ];
        chain1.shift()();

        var chain2 = [
            function() { 
                console.log("step4ba");
                fs.stat("f1.js",chain2.shift());
            },
            function(err, stats) { 
                console.log("step4bb");
                fs.stat("f1ab.js",next);
            },
        ];
        chain2.shift()();
    },
    function(err, stats) {
        if ( --fork2 )
            return;
        console.log("done");
    },
];
chain.shift()();
15 голосов
/ 18 ноября 2013

Мне очень нравится async.js для этой цели.

Проблема решается командой водопада:

водопад (задачи, [обратный вызов]))

Запускает массив функций последовательно, каждая из которых передает свои результаты следующему в массиве.Однако, если какая-либо из функций передает ошибку обратному вызову, следующая функция не выполняется, и основной обратный вызов немедленно вызывается с ошибкой.

Аргументы

tasks - Массив функцийДля запуска каждой функции передается обратный вызов (err, result1, result2, ...), который она должна вызывать по завершении.Первый аргумент является ошибкой (которая может быть нулевой), и любые дальнейшие аргументы будут переданы в качестве аргументов для следующей задачи.callback (err, [results]) - необязательный обратный вызов, запускаемый после завершения всех функций.Для этого будут переданы результаты обратного вызова последней задачи.

Пример

async.waterfall([
    function(callback){
        callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback){
        callback(null, 'three');
    },
    function(arg1, callback){
        // arg1 now equals 'three'
        callback(null, 'done');
    }
], function (err, result) {
    // result now equals 'done'    
});

Что касается переменных req, res, они будут совместно использоваться в той же области видимости, что и функция (req, res) {}, который заключил весь вызов async.waterfall.

Мало того, async очень чист.Я имею в виду, что я изменяю множество таких случаев:

function(o,cb){
    function2(o,function(err, resp){
        cb(err,resp);
    })
}

На первый:

function(o,cb){
    function2(o,cb);
}

Затем на этот:

function2(o,cb);

Затем наthis:

async.waterfall([function2,function3,function4],optionalcb)

Это также позволяет очень быстро вызывать многие готовые функции, подготовленные для асинхронного вызова из util.js.Просто включите в цепочку то, что вы хотите сделать, убедитесь, что o, cb универсально обработан.Это значительно ускоряет весь процесс кодирования.

11 голосов
/ 21 ноября 2010

Вам нужно немного синтаксического сахара. Проверьте это:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = ["<h1>Demo page</h1>"];
  var pushHTML = html.push.bind(html);

  Queue.push( getSomeData.partial(client, pushHTML) );
  Queue.push( getSomeOtherData.partial(client, pushHTML) );
  Queue.push( getMoreData.partial(client, pushHTML) );
  Queue.push( function() {
    res.write(html.join(''));
    res.end();
  });
  Queue.execute();
}); 

Довольно аккуратно , не так ли? Вы можете заметить, что HTML стал массивом. Это отчасти потому, что строки являются неизменяемыми, поэтому лучше буферизовать вывод в массиве, чем отбрасывать все большие и большие строки. Другая причина заключается в другом хорошем синтаксисе с bind.

Queue в примере на самом деле является лишь примером и вместе с partial может быть реализован следующим образом

// Functional programming for the rescue
Function.prototype.partial = function() {
  var fun = this,
      preArgs = Array.prototype.slice.call(arguments);
  return function() {
    fun.apply(null, preArgs.concat.apply(preArgs, arguments));
  };
};

Queue = [];
Queue.execute = function () {
  if (Queue.length) {
    Queue.shift()(Queue.execute);
  }
};
7 голосов
/ 01 августа 2012

Я влюблен Async.js с тех пор, как я его нашел.Он имеет функцию async.series, которую можно использовать, чтобы избежать длительного вложения.

Документация: -


серия (задачи, [обратный вызов])

Запуск массива последовательных функций, каждая из которых выполняется после завершения предыдущей функции.[...]

Аргументы

tasks - Массив функций для запуска, каждой функции передается обратный вызов, который она должна вызвать по завершении.callback(err, [results]) - Дополнительный обратный вызов, запускаемый после завершения всех функций.Эта функция получает массив всех аргументов, переданных обратным вызовам, используемым в массиве.


Вот как мы можем применить его к вашему примеру кода: -

http.createServer(function (req, res) {

    res.writeHead(200, {'Content-Type': 'text/html'});

    var html = "<h1>Demo page</h1>";

    async.series([
        function (callback) {
            getSomeData(client, function (someData) { 
                html += "<p>"+ someData +"</p>";

                callback();
            });
        },

        function (callback) {
            getSomeOtherData(client, function (someOtherData) { 
                html += "<p>"+ someOtherData +"</p>";

                callback(); 
            });
        },

        funciton (callback) {
            getMoreData(client, function (moreData) {
                html += "<p>"+ moreData +"</p>";

                callback();
            });
        }
    ], function () {
        res.write(html);
        res.end();
    });
});
6 голосов
/ 27 июня 2013

Самый простой синтаксический сахар, который я видел, это обещание узла.

npm установить узел-обещание || мерзавец клон https://github.com/kriszyp/node-promise

Используя это, вы можете связать асинхронные методы как:

firstMethod().then(secondMethod).then(thirdMethod);

Возвращаемое значение каждого доступно в качестве аргумента в следующем.

3 голосов
/ 21 ноября 2010

То, что вы сделали, - это возьмите асинхронный шаблон и примените его к 3 функциям, вызываемым последовательно, каждая из которых ожидает завершения предыдущей перед запуском - т.е. вы сделали их синхронными .Суть асинхронного программирования в том, что вы можете запускать сразу несколько функций, и вам не нужно ждать завершения каждой из них.

, если getSomeDate () не предоставляет ничего для getSomeOtherDate (), который не выполняетпредоставьте что-нибудь для getMoreData (), тогда почему бы вам не вызывать их асинхронно, поскольку js позволяет, или если они взаимозависимы (а не асинхронны) записать их как одну функцию?

Вам не нужно использовать вложение дляуправлять потоком - например, заставить каждую функцию завершаться, вызывая общую функцию, которая определяет, когда все 3 завершили, а затем отправляет ответ.

2 голосов
/ 23 декабря 2015

ад обратного вызова можно легко избежать в чистом JavaScript с закрытием.В приведенном ниже решении предполагается, что все обратные вызовы следуют за сигнатурой функции (ошибка, данные).

http.createServer(function (req, res) {
  var modeNext, onNext;

  // closure variable to keep track of next-callback-state
  modeNext = 0;

  // next-callback-handler
  onNext = function (error, data) {
    if (error) {
      modeNext = Infinity;
    } else {
      modeNext += 1;
    }
    switch (modeNext) {

    case 0:
      res.writeHead(200, {'Content-Type': 'text/html'});
      var html = "<h1>Demo page</h1>";
      getSomeDate(client, onNext);
      break;

    // handle someData
    case 1:
        html += "<p>"+ data +"</p>";
        getSomeOtherDate(client, onNext);
        break;

    // handle someOtherData
    case 2:
      html += "<p>"+ data +"</p>";
      getMoreData(client, onNext);
      break;

    // handle moreData
    case 3:
      html += "<p>"+ data +"</p>";
      res.write(html);
      res.end();
      break;

    // general catch-all error-handler
    default:
      res.statusCode = 500;
      res.end(error.message + '\n' + error.stack);
    }
  };
  onNext();
});
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...