了解node.js的asynchronous性 – for循环与嵌套的callback

我是nodejs的新手,并试图理解它的asynchronous思想。 在下面的代码片段中,我试图从mongodb数据库中随机获取两个文档。 它工作正常,但由于嵌套的callback函数,看起来非常难看。 如果我想得到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-loop代替,但是,下面的代码不起作用,我想我误解了asynchronous方法的想法。

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

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

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

tl; dr – 问题是由于在循环完成前无法完成的asynchronous函数( dataset.findOne )上运行一个循环而造成的。 您需要像async (正如其他答案中所build议的)那样使用库,或者像第一个代码示例中那样使用callback。

循环使用同步function

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

 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,您将以这种方式得到结果。

为了说明,我们来试试这个代码:

 function sleep(time){ var stop = new Date().getTime(); while(new Date().getTime() < stop + time) {} } for( i = 0 ; i < 5 ; i++ ){ sleep(1000); } 

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

这是因为sleep块中的while循环…在将控制权返回给for循环之前迭代直到经过了几毫秒的time

循环使用asynchronousfunction

问题的根源在于dataset.findOne是asynchronous的…这意味着它在数据库返回结果之前将控制权交还给循环。 findOne方法需要一个callbackfunction(err, doc)匿名function(err, doc) )来创build一个闭包。

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

但底线是,asynchronous调用将查询发送到数据库。 因为事务需要一些时间,并且它有一个可以接受查询结果的callback函数,所以它将控制权交还给for循环。 (重要的是:这是节点的“事件循环”,与“asynchronous编程”的交叉点,Node通过允许asynchronous行为来提供一个非阻塞的环境。

我们来看一个asynchronous问题如何影响我们的例子:

 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行花费的时间要长。

那么会发生什么呢?最后一行是第一个匿名callback触发之前触发的。 当它发射,循环已经完成, i等于5 。 所以你在这里看到的是循环已经完成,即使交给setTimeout的匿名函数仍然可以访问i的值。 (这是行动中的“closures”…)

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

 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和承诺可以把在这种情况下创build的“callback地狱”变成一个相对干净的解决scheme…它更容易阅读,更容易看到asynchronous的事情发生的地方,当你需要进行编辑,你没有担心你在/编辑/等等的callback级别。

你可以使用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,

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