理解node.js异步性 - 用于循环与嵌套回调

时间:2014-07-03 16:37:47

标签: javascript node.js mongodb callback

我是nodejs的新手,并且正在尝试了解它的异步想法。在下面的代码片段中,我试图从mongodb数据库中随机获取两个文档。它工作正常,但由于嵌套的回调函数看起来很丑陋。如果我想获得100个文件而不是2个,那将是一场灾难。

app.get('/api/two', function(req, res){
        dataset.count(function(err, count){
                var docs = [];
                var rand = Math.floor(Math.random() * count);
                dataset.findOne({'index':rand}, function(err, doc){
                        docs.push(doc);
                        rand = Math.floor(Math.random() * count);
                        dataset.findOne({'index':rand}, function(err, doc1){
                                docs.push(doc1);
                                res.json(docs);
                        });
                });
        });
});

所以我尝试使用for循环,但是,下面的代码不起作用,我想我误解了异步方法的想法。

app.get('/api/two', function(req, res){
        dataset.count(function(err, count){
                var docs = []
                for(i = 0; i < 2 ; i++){
                        var rand = Math.floor(Math.random() * count);
                        dataset.findOne({'index':rand}, function(err, doc){
                                docs.push(doc);
                        });
                }
                res.json(docs);
        });
});

任何人都可以帮助我,并向我解释为什么它不起作用?非常感谢你。

3 个答案:

答案 0 :(得分:2)

任何人都可以帮助我,并向我解释为什么它不起作用?

tl; dr - 问题是由在循环完成之前无法完成的异步函数(dataset.findOne)上运行循环引起的。您需要使用像async这样的库(如其他答案所示)或回调来处理这个问题,如第一个代码示例所示。

循环同步功能

这个可能听起来很迂腐,但了解同步和异步世界中循环之间的差异非常重要。考虑这个同步循环:

var numbers = [];
for( i = 0 ; i < 5 ; i++ ){
 numbers[i] = i*2;
}
console.log("array:",numbers);

在我的系统上,输出:

array: [ 0, 2, 4, 6, 8 ]

这是因为numbers[i]的赋值发生在循环迭代之前。对于任何同步(“阻塞”)赋值/函数,您将以这种方式获得结果。

为了便于说明,我们试试这段代码:

function sleep(time){
    var stop = new Date().getTime();
    while(new Date().getTime() < stop + time) {}
}

for( i = 0 ; i < 5 ; i++ ){
    sleep(1000);
}

如果你把手表拿出来或扔了一些console.log消息,你会看到“睡觉”5秒钟。

这是因为while中的sleep循环阻塞......它会循环,直到time毫秒已经过去,然后才将控制权返回给for循环。

循环异步函数

问题的根源是dataset.findOne是异步的......这意味着它在数据库返回结果之前将控制权传递回循环findOne方法接受一个创建闭包的回调(匿名function(err, doc))。

在这里描述闭包超出了这个答案的范围,但如果你搜索这个网站或使用你最喜欢的搜索引擎“javascript关闭”,你将获得大量的信息。

但最重要的是,异步调用将查询发送到数据库。因为事务将花费一些时间并且它具有可以接受查询结果的回调,所以它将控制权交还给for循环。 (重要:这是节点的“事件循环”,它与“异步编程”的交集发挥作用。节点通过允许这样的异步行为提供非阻塞环境。)

让我们看一下异步问题如何让我们兴奋的例子:

for( i = 0 ; i < 5 ; i++ ){
    setTimeout(
       function(){console.log("I think I is: ", i);} // anonymous callback
       ,1  // wait 1ms before using the callback function
    )
}

console.log("I am done executing.")

您将获得如下所示的输出:

I am done executing.
I think I is:  5
I think I is:  5
I think I is:  5
I think I is:  5
I think I is:  5

这是因为setTimeout得到一个函数来调用...所以即使我们只说“等待一毫秒”,这仍然比循环迭代5次并继续到最后一次所需的时间更长console.log行。

然后,发生的事情是,在第一个匿名回调触发之前,最后一行会触发。当 触发时,循环结束且i等于5。所以你在这里看到的是循环已经完成,已经移动,即使传递给setTimeout的匿名函数仍然可以访问i的值。 (这是“关闭”的行动......)

如果我们采用这个概念并用它来考虑你的第二个“破损”代码示例,我们就会明白为什么你没有得到你期望的结果。

app.get('/api/two', function(req, res){
        dataset.count(function(err, count){
                var docs = []
                for(i = 0; i < 2 ; i++){
                        var rand = Math.floor(Math.random() * count);

                        // THIS IS ASYNCHRONOUS.
                        // findOne gets a callback...
                        // hands control back to the for loop...
                        // and later pushes info into the "doc" array...
                        // too late for res.json, at least...

                        dataset.findOne({'index':rand}, function(err, doc){
                                docs.push(doc);
                        });
                }

                // THE LOOP HAS ENDED BEFORE any of the findOne callbacks fire...
                // There's nothing in 'docs' to be sent back to the client.  :(

                res.json(docs);
        });
});

async,promises和其他类似库是一个好工具的原因是它们有助于解决您所面临的问题。 async和承诺可以将在这种情况下创建的“回调地狱”变成一个相对干净的解决方案......它更容易阅读,更容易看到异步事件发生的位置,以及何时需要编辑你不必担心你在/编辑/等等的回调级别。

答案 1 :(得分:1)

您可以使用async模块。例如:

var async = require('async');

async.times(2, function(n, next) {
  var rand = Math.floor(Math.random() * count);
  dataset.findOne({'index':rand}, function(err, doc) {
    next(err, doc);
  });
}, function(err, docs) {
  res.json(docs);
});

如果您想获得100个文档,只需将Async.times(2,更改为Async.times(100,

答案 2 :(得分:1)

如上所述的异步模块是一个很好的解决方案。发生这种情况的原因是因为常规Javascript for循环是同步的,而您对数据库的调用是异步的。 for循环不知道你想要等到检索数据进入下一次迭代,所以它只是继续进行,并且比数据检索更快完成。