node.js在遇到“循环+异步”时的注意事项
转载自http://blog.csdn.net/fangjian1204/article/details/50585073
nodejs的特征
nodejs的最大特征就是一切都是基于事件的,从而导致一切都是异步的。nodejs的速度为什么快,其原理和nginx一样,他们都是通过事件回调来处理请求的,从而导致了整个处理过程中,不会阻塞nodejs,因此,其在同一时间内可以处理大量的请求,而这种优越性在你的请求是IO密集型的情况下,表现的尤为突出。下面的例子简单说明了基于异步事件的nodejs的处理流程:
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; console.log("输出:在这里处理数据库操作的结果"); }); console.log("输出:这是数据库操作后的语句"); };
使用过nodejs的程序员应该很容易知道,该函数的输出结果是:
输出:这是数据库操作后的语句
输出:在这里处理数据库操作的结果
原因很简单,上面的查询语句并不是立即执行,而是放入待执行的队列中就立即返回,然后继续执行后面的语句;当数据库操作结束之后,会触发某个事件,告诉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了。