Node.js: лучший способ выполнить несколько асинхронных операций, а затем сделать что-то еще?

В следующем коде я пытаюсь сделать несколько (около 10) HTTP-запросов и RSS-анализов за один раз.

Я использую стандартную конструкцию forEach для массива URI, к которому мне нужно получить доступ и проанализировать результат.

Код:

var articles;

feedsToFetch.forEach(function (feedUri)
{   
        feed(feedUri, function(err, feedArticles) 
        {
            if (err)
            {
                throw err;
            }
            else
            {
                articles = articles.concat(feedArticles);
            }
        });
 });

 // Code I want to run once all feedUris have been visited

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

Итак, мой вопрос: как лучше всего справиться с такой ситуацией в node.js.

Желательно без блокировки! (Я все еще хочу эту молниеносную скорость). Это обещания или что-то другое?

Спасибо, Дэнни.


person dannybrown    schedule 09.10.2014    source источник
comment
Да, обещания — это самый простой способ. Если вы предпочитаете создавать собственное решение с использованием стандартного JS, ">этот мой ответ можно легко адаптировать.   -  person Amadan    schedule 09.10.2014
comment
пожалуйста, покажите код функции feed().   -  person jfriend00    schedule 09.10.2014
comment
@ Амадан, может быть, это личное предпочтение, но я не думаю, что обещания - самый простой способ. Я предоставил ответ, чтобы расширить это.   -  person Mulan    schedule 09.10.2014
comment
Вы разобрались: используйте счетчик и посчитайте количество сделанных и выполненных запросов. Это в основном то, что асинхронность и промисы делают внутри. Примерно за год до написания асинхронной библиотеки я написал этот ответ, чтобы решить именно эту проблему: stackoverflow.com/questions/4631774/ . Это более продвинутая реализация, позволяющая запускать пакеты асинхронных операций: " title="процессная цепочка функций без блока пользовательского интерфейса"> stackoverflow.com/questions/13250746/   -  person slebetman    schedule 09.10.2014
comment
@naomik: я бы сказал, что личное предпочтение - я не вижу ответа ааросила намного сложнее, чем ваш.   -  person Amadan    schedule 09.10.2014
comment
@DanTonyBrown для вас имеет значение, в каком порядке статьи заканчиваются? Вариант serial от @naomik ниже - единственный, который сохранит порядок, если я не ошибаюсь.   -  person Stop Slandering Monica Cellio    schedule 09.10.2014
comment
@sequoiamcdowell с промисами, которые были бы стандартом для цикла ... промисы - это настоящая абстракция для упорядочивания вещей - это намного более правильно и полно, чем модуль «асинхронный», большинство современных языков имеют их под разными именами, и они были широкое распространение.   -  person Benjamin Gruenbaum    schedule 10.10.2014
comment
Промисы @BenjaminGruenbaum широко используются [всеми, кроме IE]. Мне нравится, как вы критикуете мой ответ за использование библиотеки, полностью игнорируя тот факт, что я также предоставил собственное решение. В то же время вы в основном говорите, что используйте что-то, что не поддерживается в IE, или получите полифилл (см.: lib).   -  person Mulan    schedule 10.10.2014
comment
Я исправляюсь - .map сохраняет порядок.   -  person Stop Slandering Monica Cellio    schedule 10.10.2014


Ответы (3)


РЕШЕНИЕ БЕЗ ВЗЛОМА

Обещается быть включенным в следующую версию JavaScript

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

Bluebird также имеет .map(), который может принимать массив значений и использовать его для запуска цепочки промисов.

Вот пример использования Bluebird .map():

var Promise = require('bluebird');
var request = Promise.promisifyAll(require('request'));

function processAllFeeds(feedsToFetch) {    
    return Promise.map(feedsToFetch, function(feed){ 
        // I renamed your 'feed' fn to 'processFeed'
        return processFeed(feed) 
    })
    .then(function(articles){
        // 'articles' is now an array w/ results of all 'processFeed' calls
        // do something with all the results...
    })
    .catch(function(e){
        // feed server was down, etc
    })
}

function processFeed(feed) { 
    // use the promisified version of 'get'
    return request.getAsync(feed.url)... 
}

Обратите также внимание, что здесь не нужно использовать замыкание для накопления результатов.

Документация по Bluebird API тоже очень хорошо написана, с множеством примеров, так легче подобрать.

Как только я изучил паттерн Promise, жизнь стала намного проще. Я не могу рекомендовать это достаточно.

Кроме того, вот отличная статья о разных подходах к работе с асинхронными функциями с использованием промисов, модуля async и др.

Надеюсь это поможет!

person aarosil    schedule 09.10.2014
comment
@naomik Я обновил ответ, чтобы отразить статус отсутствия взлома - person aarosil; 10.10.2014

Взломы не требуются

Я бы порекомендовал использовать модуль async, так как он значительно упрощает такие вещи.

async предоставляет async.eachSeries в качестве асинхронной замены arr.forEach и позволяет передать done функция обратного вызова, когда она завершена. Он будет обрабатывать каждый элемент в серии, как это делает forEach. Кроме того, он будет удобно отображать ошибки в вашем обратном вызове, чтобы вам не приходилось иметь логику обработчика внутри цикла. Если вам нужна/требуется параллельная обработка, вы можете использовать async.each. .

Между вызовом async.eachSeries и обратным вызовом не будет блокировки.

async.eachSeries(feedsToFetch, function(feedUri, done) {

  // call your async function
  feed(feedUri, function(err, feedArticles) {

    // if there's an error, "bubble" it to the callback
    if (err) return done(err);

    // your operation here;
    articles = articles.concat(feedArticles);

    // this task is done
    done();
  });
}, function(err) {

  // errors generated in the loop above will be accessible here
  if (err) throw err;

  // we're all done!
  console.log("all done!");
});

В качестве альтернативы вы можете создать массив асинхронных операций и передать их async.series. Series обработает ваши результаты последовательно (не параллельно) и вызовет обратный вызов после завершения каждой функции. Единственная причина использовать это вместо async.eachSeries — это если вы предпочитаете знакомый синтаксис arr.forEach.

// create an array of async tasks
var tasks = [];

feedsToFetch.forEach(function (feedUri) {

  // add each task to the task array
  tasks.push(function() {

    // your operations
    feed(feedUri, function(err, feedArticles) {
      if (err) throw err;
      articles = articles.concat(feedArticles);
    });
  });
});

// call async.series with the task array and callback
async.series(tasks, function() {
 console.log("done !");
});

Или вы можете Roll Your Own™

Возможно, вы слишком амбициозны или не хотите полагаться на зависимость async. Может быть, вам просто скучно, как и мне. В любом случае, я намеренно скопировал API async.eachSeries, чтобы было проще понять, как это работает.

Как только мы удалим здесь комментарии, у нас останется всего 9 строк кода, которые можно повторно использовать для любого массива, который мы хотим обрабатывать асинхронно! Он не будет изменять исходный массив, ошибки могут быть отправлены для «короткого замыкания» итерации, и можно использовать отдельный обратный вызов. Он также будет работать с пустыми массивами. Совсем немного функционала всего за 9 строк :)

// void asyncForEach(Array arr, Function iterator, Function callback)
//   * iterator(item, done) - done can be called with an err to shortcut to callback
//   * callback(done)       - done recieves error if an iterator sent one
function asyncForEach(arr, iterator, callback) {

  // create a cloned queue of arr
  var queue = arr.slice(0);

  // create a recursive iterator
  function next(err) {

    // if there's an error, bubble to callback
    if (err) return callback(err);

    // if the queue is empty, call the callback with no error
    if (queue.length === 0) return callback(null);

    // call the callback with our task
    // we pass `next` here so the task can let us know when to move on to the next task
    iterator(queue.shift(), next);
  }

  // start the loop;
  next();
}

Теперь давайте создадим образец асинхронной функции для использования с ним. Здесь мы имитируем задержку с setTimeout 500 мс.

// void sampleAsync(String uri, Function done)
//   * done receives message string after 500 ms
function sampleAsync(uri, done) {

  // fake delay of 500 ms
  setTimeout(function() {

    // our operation
    // <= "foo"
    // => "async foo !"
    var message = ["async", uri, "!"].join(" ");

    // call done with our result
    done(message);
  }, 500);
}

Хорошо, давайте посмотрим, как они работают!

tasks = ["cat", "hat", "wat"];

asyncForEach(tasks, function(uri, done) {
  sampleAsync(uri, function(message) {
    console.log(message);
    done();
  });
}, function() {
  console.log("done");
});

Выход (задержка 500 мс перед каждым выходом)

async cat !
async hat !
async wat !
done
person Mulan    schedule 09.10.2014
comment
обратите внимание, что это node/v8 vNext, он будет иметь выход. Найдите это. - person bryanmac; 09.10.2014
comment
asyncEach здесь ведет себя больше как async.eachSeries, чем async.each fwiw. async.each будет запускать http-запросы (или что-то еще) так быстро, как только может, то есть одновременно; asynSeries (или последовательное решение здесь) будет делать одно за другим. Если я не ошибаюсь. - person Stop Slandering Monica Cellio; 09.10.2014
comment
@sequoiamcdowell, хороший улов. Я переименовал пользовательский asyncEach в asyncForEach, чтобы указать, что он выполняет последовательную обработку, такую ​​​​как arr.forEach. Я также обновил async.each до async.eachSeries. Спасибо ! - person Mulan; 09.10.2014
comment
Никаких взломов не требуется... Используйте эту библиотеку - я не следую этой логике, с другой стороны, NodeJS поставляется с обещаниями, которые сделают это тривиальным. - person Benjamin Gruenbaum; 10.10.2014
comment
@BenjaminGruenbaum, использование библиотеки не означает взлом. Я также предоставил рулон вашего собственного решения. - person Mulan; 10.10.2014
comment
@BenjaminGruenbaum Я не знал, что узел поставляется с обещаниями, и моя новая установка, похоже, их не содержит. Какой сегодня универсальный способ доступа к нативным промисам в node.js без взлома? - person Stop Slandering Monica Cellio; 10.10.2014
comment
@sequoiamcdowell $ echo "Promise" | node должен дать вам ReferenceError: Promise is not defined. Это особенность. Просто добавьте bluebird и все ваши проблемы исчезнут. - person Mulan; 10.10.2014
comment
@sequoiamcdowell, какую версию Node вы используете? Это примерно с 0.11.13 - person Benjamin Gruenbaum; 06.11.2014
comment
@BenjaminGruenbaum Итак, когда вы говорите, что узел поставляется с Promises, вы имеете в виду, используете ли вы самую последнюю версию нестабильный релиз и обязательно запустите его с переключателем --harmony, чтобы включить экспериментальные функции, [узел поставляется с промисами]? Очень своеобразное определение отсутствия хаков и кораблей с :) Кстати, забавно, что вы сказали, так как примерно так как названный вами релиз является самым последним помеченным нестабильным релизом. - person Stop Slandering Monica Cellio; 06.11.2014

использование копии списка URL-адресов в качестве очереди для отслеживания прибытий упрощает: (все изменения прокомментированы)

var q=feedsToFetch.slice(); // dupe to censor upon url arrival (to track progress)

feedsToFetch.forEach(function (feedUri)
{   
        feed(feedUri, function(err, feedArticles) 
        {
            if (err)
            {
                throw err;
            }
            else
            {
                articles = articles.concat(feedArticles);
            }
            q.splice(q.indexOf(feedUri),1); //remove this url from list
            if(!q.length) done(); // if all urls have been removed, fire needy code
        });
 });

function done(){
  // Code I want to run once all feedUris have been visited

}

в конце концов, это не намного «грязнее», чем обещания, и дает вам возможность перезагрузить незавершенные URL-адреса (один только счетчик не скажет вам, какой из них не прошел). для этой простой задачи параллельной загрузки на самом деле будет добавлено больше кода в ваш проект, реализующий Promises, чем простая очередь, и Promise.all() находится не в самом интуитивно понятном месте, на которое можно наткнуться. Как только вы попадете в под-под-запросы или захотите лучше обрабатывать ошибки, чем крушение поезда, я настоятельно рекомендую использовать Promises, но вам не нужна ракетная установка, чтобы убить белку...

person dandavis    schedule 09.10.2014