2014-07-03 162 views
1

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

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); 
     }); 
}); 

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

回答

2

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

TL;博士 - 该问题是通过在一个异步函数(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得到一个函数调用...所以,即使我们只说“等待一毫秒“,那仍然是lo比循环重复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,承诺和其他类似的库是一个很好的工具是他们帮助解决你所面临的问题。 async和承诺可以将在这种情况下创建的“回调地狱”变成一个相对干净的解决方案...它更容易阅读,更容易看到异步情况发生的地方,以及当你需要进行编辑时,你没有担心你在/编辑/等等的回调级别。

+0

非常感谢你的详细解释! – Idealist

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,

1

上面提到的异步模块是一个很好的解决方案。发生这种情况的原因是因为正常的Javascript for循环是同步的,而对数据库的调用是异步的。 for循环并不知道你希望等待数据被检索到下一次迭代,所以它只是继续前进,并且比数据检索更快结束。