【问题标题】:Node.js: Best way to perform multiple async operations, then do something else?Node.js:执行多个异步操作的最佳方式,然后做其他事情?
【发布时间】:2014-12-03 19:22:08
【问题描述】:

在下面的代码中,我试图一次发出多个(大约 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 相同时才继续,这似乎很hacky .

所以我的问题是,在 node.js 中处理这种情况的最佳方法是什么

最好没有任何形式的阻塞! (我仍然想要那种极快的速度)。是承诺还是别的什么?

谢谢, 丹尼

【问题讨论】:

  • 是的,promise 是最简单的方法。如果您更愿意使用标准 JS 编写自己的解决方案,this answer of mine 可以轻松适应。
  • 请显示feed()函数的代码。
  • @Amadan,也许这是个人喜好,但我不认为 Promises 是“最简单”的方式。我已经提供了一个答案以对此进行扩展。
  • 您已经弄清楚了:使用计数器并计算发出和完成的请求数。这基本上是 async 和 promises 在内部所做的。在编写异步库大约一年前,我写了这个答案来解决这个确切的问题:stackoverflow.com/questions/4631774/…。这是一个更高级的实现,允许您启动批量异步操作:stackoverflow.com/questions/13250746/…
  • @naomik:我想说的是个人喜好——我认为 aarosil 的答案没有比你的复杂得多。

标签: javascript node.js promise


【解决方案1】:

免黑客解决方案

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 介绍了使用 Promise、async 模块等处理异步函数的不同方法

希望这会有所帮助!

【讨论】:

  • @naomik 我更新了答案以反映无黑客状态
【解决方案2】:

无需破解

我会推荐使用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 将在 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,以便于理解它是如何工作的。

一旦我们删除了这里的 cmets,我们就只有 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();
}

现在让我们创建一个示例异步函数来使用它。我们将在此处使用 500 毫秒的 setTimeout 来伪造延迟。

// 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

【讨论】:

  • 注意是node/v8 vNext,它会有yield。搜索那个。
  • 这里的 asyncEach 的行为更像 async.eachSeries 而不是 async.each fwiw。 async.each 将尽可能快地触发 http 请求(或其他),即同时触发; asynSeries(或此处的“串行”解决方案)将一个接一个地执行。除非我弄错了。
  • @sequoiamcdowell,很好。我已将自定义asyncEach 重命名为asyncForEach,以暗示它像arr.forEach 一样进行串行处理。我还将async.each 更新为async.eachSeries。谢谢!
  • “无需 hacks...使用这个库” - 我不遵循这个逻辑,另一方面,NodeJS 提供了可以让这变得微不足道的承诺。
  • @BenjaminGruenbaum 因此,当您说节点“附带”承诺时,您的意思是“如果您使用 very latest unstable 版本,并确保使用 @987654351 运行它@ 开关以启用实验性功能,[节点附带承诺]”?这是对“no hacks”和“ships with”的一个非常特殊的定义 :) 顺便说一句,你应该说“since about”很有趣,因为你命名的版本是最新标记的不稳定版本.
【解决方案3】:

使用 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 的机会(单独的计数器不会告诉您哪个(s)失败了)。对于这个简单的并行下载任务,它实际上会向您的项目添加更多代码来实现 Promises,而不是简单的队列,并且 Promise.all() 并不是最直观的地方。一旦你进入子子查询,或者想要比 trainwreck 更好的错误处理,我强烈建议使用 Promises,但你不需要火箭发射器来杀死一只松鼠......

【讨论】:

    猜你喜欢
    • 2015-11-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-17
    • 2016-10-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-14
    相关资源
    最近更新 更多