nodejs循环的陷阱



Node.js 的异步机制由事件和回调函数实现,一开始接触可能会感觉违反常规,但习惯以后就会发现还是很简单的。然而这之中其实暗藏了不少陷阱,一个很容易遇到的问题就是循环中的回调函数,初学者经常容易陷入这个圈套。让我们从一个例子开始说明这个问题。
  //forloop.js

 var  fs = require('fs');
var  files = ['a.text', 'b.text', 'c.text'];
for  (var  i = 0 ; i < files.length; i    ) {
    fs.readFile(files[i], 'utf-8',  function (err, contents) {
        console.log(files[i], ': ', contents);
    });
}


  这段代码的功能很直观,就是依次读取文件 a.txt、b.txt 、c.txt ,并输出文件名和内容。假设这三个文件的内容分别是 AAA 、BBB 和 CCC,那么我们期望的输出结果就是:
  a.txt: AAA
  b.txt: BBB
  c.txt: CCC
  可是我们运行这段代码的结果是怎样的呢?竟然是这样的结果:
  undefined: AAA
  undefined: BBB
  undefined: CCC
  这个结果说明文件内容正确输出了,而文件名却不对,也就意味着,contents 的结果是正确的,但  files[i] 的值是  undefined。这怎么可能呢,文件名不正确却能读取文件内容?既然难以直观地理解,我们就把 files[i]  分解并打印出来看看,在读取文件的回调
  函数中分别输出 files、i 和  files[i] 。

  //forloopi.js

var  fs = require('fs');
var  files = ['a.text', 'b.text', 'c.text'];
for  (var  i = 0 ; i < files.length; i    ) {
    fs.readFile(files[i], 'utf-8',  function (err, contents) {
        console.log(files);
        console.log(i);
        console.log(files[i]);
    });
}


  运行修改后的代码,结果如下:
  [ 'a.txt', 'b.txt', 'c.txt' ]
  3
  undefined
  [ 'a.txt', 'b.txt', 'c.txt' ]
  3
  undefined
  [ 'a.txt', 'b.txt', 'c.txt' ]
  3
  undefined
  看到这里是不是有点启发了呢?三次输出的 i 的值都是 3 ,超出了  files 数组的下标范围,因此 files[i]  的值就是 undefined 了。这种情况通常会在 for  循环结束时发生,例如  for (var i = 0;  i < files.length; i  ),退出循环时 i  的值就是 files.length  的值。既然 i 的值是 3 ,那么说明了事实上 fs.readFile  的回调函数中访问到的  i 值都是循环退出以后的,因此不能分辨。而  files[i] 作为 fs.readFile  的第一个参数在循环中就传递了,所以文件可以被定位到,而且可以显示出文件的内容。 现在问题就明朗了:原因是3 次读取文件的回调函数事实上是同一个实例,其中引用到的  i  值是上面循环执行结束后的值,因此不能分辨。如何解决这个问题呢?我们可以利用 JavaScript  函数式编程的特性,手动建立一个闭包:
  //forloopclosure.js

  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);
}


  
  上面代码在  for 循环体中建立了一个匿名函数,将循环迭代变量  i 作为函数的参数传递并调用。由于运行时闭包的存在,该匿名函数中定义的变量(包括参数表)在它内部的函数(  fs.readFile 的回调函数)执行完毕之前都不会释放,因此我们在其中访问到的 i 就分别是不同的闭包实例,这个实例是在循环体执行的过程中创建的,保留了不同的值。 事实上以上这种写法并不常见,因为它降低了程序的可读性,故不推荐使用。大多数情况下我们可以用数组的  forEach  方法解决这个问题:
  //callbackforeach.js

  

var  fs = require('fs');
var  files = ['a.text', 'b.text', 'c.text'];
files.forEach(function (filename) {
    fs.readFile(filename, 'utf-8',  function (err, contents) {
        console.log(filename,': ',contents);
    });
});

                

  最后介绍一种asnyc的方式

var async=require('async') ;


async.eachSeries(files, function (item, callback) {
    console.log(item)
    fs.readFile(item, 'utf-8',  function (err, contents) {
        console.log(item,': ',contents);
        callback(null)
    });
}, function () {
    console.log("OK");
    //process.exit(1);
});