2014-07-03 184 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循環並不知道你希望等待數據被檢索到下一次迭代,所以它只是繼續前進,並且比數據檢索更快結束。