Node.js:执行多个异步操作的最佳方式,然后执行其他操作?

时间:2014-10-09 00:46:15

标签: javascript node.js promise

在下面的代码中,我试图一次性发出多个(大约10个)HTTP请求和RSS解析。

我在我需要访问的URI数组上使用标准forEach构造并解析结果。

代码:

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 中处理此类情况的最佳方法是什么。

最好没有任何形式的阻挡! (我仍然希望那种超快的速度)。这是承诺还是其他什么?

谢谢, 丹尼

3 个答案:

答案 0 :(得分:10)

没有必要的黑客

我建议使用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 !");
});

或者你可以自己动手

也许你感到更加野心勃勃,或者你可能不想依赖async依赖。也许你只是像我一样无聊。无论如何,我故意复制了async.eachSeries的API,以便于理解其工作原理。

我们删除了这里的评论后,我们只有 9行代码<​​/ strong>,可以重复用于我们想要异步处理的任何数组!它不会修改原始数组,可以将错误发送到“短路”迭代,并且可以使用单独的回调。它也适用于空数组。只有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 ms)

async cat !
async hat !
async wat !
done

答案 1 :(得分:9)

无解决方案

Promises to be included in next JavaScript version

流行的Promise库为这个确切的用例提供了一个.all()方法(等待一堆异步调用完成,然后再执行其他操作)。它非常适合您的场景

Bluebird还有.map(),它可以获取一系列值并使用它来启动Promise链。

以下是使用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 Docs也写得很好,有很多例子,所以它更容易上手。

一旦我学会了Promise模式,它就让生活变得如此简单。我不能推荐它。

另外,这里有a great article关于使用promises处理异步函数的不同方法,async模块和其他

希望这有帮助!

答案 2 :(得分:1)

使用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

}

最后,这并不是那么多&#34;更脏&#34;比承诺,并提供机会重新加载未完成的网址(一个单独的计数器不会告诉你哪一个失败)。对于这个简单的并行下载任务,它实际上会为你的项目实现Promises而不是一个简单的队列添加更多的代码,并且Promise.all()不是最容易发现的最直观的地方。一旦你进入子查询,或想要比火车残骸更好的错误处理,我强烈建议使用Promises,但你不需要火箭发射器来杀死一只松鼠......