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了。

posted @ 2017-09-19 09:46  申小贺  阅读(341)  评论(0编辑  收藏  举报