Nodejs中“循环+异步”好深的坑!!
这⼏天在搞Nodejs从数据库中获取数据进⾏格式转换,折腾了好⼏个⽇⽇夜夜,各种循环+异步操作,最后发现这玩意好恶⼼,怎么每次运⾏结果都会变,有时报错哪个值undefined有时正常,简直吐⾎,经历⽆数次测试,慢慢发现原因。
看到这篇博客描述地挺详细的,就转载记录下吧;
感谢原博主,原⽂链接
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++) {
console.log(files[i] + ': ' + contents);
});
}
假设这三个⽂件的内容分别为:AAA、BBB、CCC,我们期望的结果是:
<: AAA
<: BBB
<: 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) {
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) {
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;
});
});
}
};
上⾯的例⼦是先查询商品的信息,然后对每⼀个商品,⽤其id去查询标签列表,并添加到每条商品信息中。上⾯返回的结果真的会和期望的⼀样吗?然⽽,最后仅仅返回了不包含标签的商品信息,即还没等到内层查询执⾏结束,der()⽅法就已经返回了。虽然我们保证了第⼆条查询在第⼀条查询结束之后再执⾏,但我们⽆法保证返回语句在第⼆条查询结束之后再返回。具体的解决⽅法可能有多种,这⾥我们使⽤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);
});
js合并两个数组}, function(err,results) {
});
});
};
此时,第⼆个sql语句每次查询到的tag被保存到item中,等所有的查询结束后,调⽤callback(null, item);即把所有的数据传递给results参数,最后统⼀发送给浏览器。此时发送的商品中,就包含了商品标签tag了。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。