最近在用nodjs寫后端,碰到了一個很常見的問題,在一個循環里面如果有回調,那么如何才能把循環取到的值傳遞到循環體內的函數呢?如果按照以前同步的方法,很容易,直接在形參里面就傳過去了,但是nodejs會先把循環走完,再執行回調(不嚴謹的說法,其實是異步執行了,循環不一定走完了),這樣每次拿到的值就是最后一次循環的值了,完全沒法用。。。
磕磕碰碰好幾天,看到這篇博客挺詳細的,轉載記錄一下。原博地址:https://blog.csdn.net/fangjian1204/article/details/50585073
nodejs的特征
nodejs的最大特征就是一切都是基於事件的,從而導致一切都是異步的。nodejs的速度為什么快,其原理和nginx一樣,他們都是通過事件回調來處理請求的,從而導致了整個處理過程中,不會阻塞nodejs,因此,其在同一時間內可以處理大量的請求,而這種優越性在你的請求是IO密集型的情況下,表現的尤為突出。下面的例子簡單說明了基於異步事件的nodejs的處理流程:
1 var send_data = function(req,res){ 2 sql = 'SELECT gid,name,image_url,price,create_time,describes,selluid FROM goods WHERE status=? LIMIT ?,?'; 3 connection.query(sql, [0,0,6], function(err, rows, fields) { 4 if (err) throw err; 5 console.log("輸出:在這里處理數據庫操作的結果"); 6 }); 7 console.log("輸出:這是數據庫操作后的語句"); 8 };
使用過nodejs的程序員應該很容易知道,該函數的輸出結果是:
1 輸出:這是數據庫操作后的語句 2 輸出:在這里處理數據庫操作的結果
原因很簡單,上面的查詢語句並不是立即執行,而是放入待執行的隊列中就立即返回,然后繼續執行后面的語句;當數據庫操作結束之后,會觸發某個事件,告訴nodejs數據庫操作已經完成,於是nodejs就執行原先設定的回調函數,對數據庫的執行結果進行處理。這正是nodejs高效的地方,然而,凡事總有兩面性,nodejs在高效的同時,也增加了程序員編寫程序的復雜性,因為異步程序和以往的同步程序有很大的區別,下面我們來看一個常見的注意事項。
for循環+異步操作
一個很經典的問題就是在循環中遇到回調函數:
var fs = require('fs'); var files = ['a.txt','b.txt','c.txt']; for(var i=0; i < files.length; i++) { fs.readFile(files[i], 'utf-8', function(err, contents) { console.log(files[i] + ': ' + contents); }); }
假設這三個文件的內容分別為:AAA、BBB、CCC,我們期望的結果是:
a.txt: AAA
b.txt: BBB
c.txt: CCC
而實際的結果卻是:
undefined: AAA
undefined: BBB
undefined: CCC
這是為什么呢?如果我們在循環內部把i的值打印出來,可以看出,三次輸出的數據都是3,也就是files.length的值。也就是說,fs.readFile的回調函數中訪問到的i值都是循環結束后的值,因此files[i]的值為undefined。解決此問題有很多方法,這里利用js函數編程的特性,建立一個閉包來保存每次需要的i值:
var fs = require('fs'); var files = ['a.txt','b.txt','c.txt']; for(var i=0; i < files.length; i++) { (function(i) { fs.readFile(files[i], 'utf-8', function(err, contents) { console.log(files[i] + ': ' + contents); }); })(i); }
由於運行時閉包的存在,該匿名函數中定義的變量(包括參數表)在它內部的函數(fs.readFile 的回調函數)執行完畢之前都不會釋放,因此我們在其中訪問到的 i 就分別是不同的閉包實例,這個實例是在循環體執行的過程中創建的,保留了不同的值。這里使用閉包是為了更清楚的看到上面輸出undefined的原因,其實,還可以有更簡單的方法:
var fs = require('fs'); var files = ['a.txt', 'b.txt', 'c.txt']; files.forEach(function(filename) { fs.readFile(filename, 'utf-8', function(err, contents) { console.log(filename + ': ' + contents); }); });
有關聯的多條sql查詢操作
從上面的for循環可以清楚的看到異步編程與同步編程的不同:雖然高效,但是坑很多。再比如:如果我們有需要進行兩次sql操作,但是有明確的需要,第二次必須要在第一次完成之后進行,怎么辦?這很簡單,只需要把第二次操作寫在第一次的回調函數內部即可,因為第一次的回調函數觸發的前提就是其已經執行完畢。但是如果第二次操作需要第一次操作返回的數據作為查詢條件,而且要把兩次結果合並起來返回,該如何處理呢?是如下這樣嗎?
var send_data = function(req,res){ sql = 'SELECT gid,name,image_url,price,create_time,describes,selluid FROM goods WHERE status=? LIMIT ?,?'; connection.query(sql, [0,0,6], function(err, rows, fields) { if (err) throw err; rows.forEach(function(item){ sql = "SELECT tag_name FROM tag,tag_goods WHERE tag_goods.gid=? AND tag_goods.tagid=tag.tagid"; connection.query(sql, item.gid, function(err, tags, fields){ if (err) throw err; item.tags = tags; }); }); res.render('index', {supplies:rows, login:req.session.login}); } };
上面的例子是先查詢商品的信息,然后對每一個商品,用其id去查詢標簽列表,並添加到每條商品信息中。上面返回的結果真的會和期望的一樣嗎?然而,最后僅僅返回了不包含標簽的商品信息,即還沒等到內層查詢執行結束,res.render()方法就已經返回了。雖然我們保證了第二條查詢在第一條查詢結束之后再執行,但我們無法保證返回語句在第二條查詢結束之后再返回。具體的解決方法可能有多種,這里我們使用async模塊來解決這里的同步問題。
ASync函數介紹
async主要實現了很多有用的函數,例如:
- each: 如果想對同一個集合中的所有元素都執行同一個異步操作。
- map: 對集合中的每一個元素,執行某個異步操作,得到結果。所有的結果將匯總到最終的callback里。與each的區別是,each只關心操作不管最后的值,而map關心的最后產生的值。
- series: 串行執行,一個函數數組中的每個函數,每一個函數執行完成之后才能執行下一個函數。
- parallel: 並行執行多個函數,每個函數都是立即執行,不需要等待其它函數先執行。傳給最終callback的數組中的數據按照tasks中聲明的順序,而不是執行完成的順序。
- 其它
很明顯,這里我們可以使用map函數來實現我們的需求。該方法的原型為:map(arr, iterator(item, callback), callback(err, results));也就是說,我們用arr中的每一個元素item迭代調用iterator()方法,並把每次的結果保存下來,當迭代完之后,把結果匯聚起來給results調用callback()方法。應用此方法,我們的程序修改為:
var async = require('async'); var send_data = function(req,res){ sql = 'SELECT gid,name,image_url,price,create_time,describes,selluid FROM goods WHERE status=? LIMIT ?,?'; connection.query(sql, [0,0,6], function(err, rows, fields) { if (err) throw err; async.map(rows, function(item, callback) { sql = "SELECT tag_name FROM tag,tag_goods WHERE tag_goods.gid=? AND tag_goods.tagid=tag.tagid"; connection.query(sql, item.gid, function(err, tags, fields){ item.tags = tags; callback(null, item); }); }, function(err,results) { res.render('index', {supplies:results, login:req.session.login}); }); }); };
此時,第二個sql語句每次查詢到的tag被保存到item中,等所有的查詢結束后,調用callback(null, item);即把所有的數據傳遞給results參數,最后統一發送給瀏覽器。此時發送的商品中,就包含了商品標簽tag了。