1、回调与循环的陷阱解析
今天拜读BYVoid大神的《Node.js开发指南》小有收获,特地写篇博文出来给大家伙分享一下。
文中提到了一个Node.js的回调与循环的陷阱。
下面是书上的源码
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
下面是实际运行起来的输出结果
undefined: AAA
undefined: BBB
undefined: CCC
很显然这跟我们想要的结果有很大的偏差,那到底是什么造成这种偏差呢。
可以看到上面的文件名都是undefined ,也可以说作根本没有获取到文件名,那为什么没有获取到文件名却可以获取到文件里的内容呢。
在书中作者只简单的讲解了为什么是Undefined,却没有说明为什么能够获取到文本的内容。这里我通过结合node 的异步机制给大家讲解一下,希望大家也有所收获。
为什么没有获取到文件名
首先我们必须思考第一个问题,为什么没有获取到文件名。
结合书上的代码,我直接贴出来。
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);
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
可以看见三次打印出来的结果都是3,这说明当打印数组下标的时候,整个循环已经结束了,由于js没有{}作用域,所以i是一直都没有被释放,因此我们打印除了3 次 3!因此也就可以解释为什么我们获取不到文件名了,因为数组越界了。
而之所以会数组越界,是因为node本身的异步机制起的作用。
在Node里,所有对文件的读取写入操作都有sync, async两个版本,上面我们用了async ,因此我们读取文件是一个异步事件。
在进入for循环之后,node将异步事件带到另一个地方去处理,并将处理完的结果进入事件队列,由事件循环取出,最后放入执行栈,调用回调函数。
便于理解,这里我又要放一次这张图了
所以,其实调用回调函数的时候,我们的同步事件已经全部执行完毕,而i一直没有被释放,所以打印出来是3。
为什么在没有获取到文件名的情况下却能获取到文件的实际内容
第一个问题解决了,我们就必须要解决第二个问题,为什么在没有获取到文件名的情况下,却能够获取到文件内容?
我相信答案已经显而易见了,这都是由node的异步机制决定的。
为什么这么说,下面我就给大家一行一行解释一下。
我先把上面的源码粘下来,方便大家看
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);
});
}
从进入for循环开始,主线程就遇到了一个异步事件,解释器将它带去别的地方进行处理,注意这个操作!解释器只是将它带去别的地方处理,因为for指定循环三次,所以第一次的时候i的值是0,被获取到传入files[i]当中,所以我们这时其实是获取到了文件名,接下来它就被带去了别的地方进行处理,当然,整个操作中我理所当然的获取到了文件的实际内容,并且循环了三次,三次我们都获取到了。
fs.readFile(files[i], 'utf-8', function(err, contents){});
接下来这个就厉害了
在回调函数中的函数体中,我们打印了获取到了content,好像并没有什么错,可这才是陷阱的开始
console.log(files[i] + ': ' + contents);
我们根据文件名称获取到了文件实际的内容,因此获取到的内容会进入事件队列,待执行栈空闲的时候,事件循环把获取到的内容取出来,然后最后一步调用回调函数。
但是在我们调用回调函数的时候,整个循环都已经结束了,我们只能获取到循环过后的i,即3,但实际的files[3]已经数组下标越界,因此,我们只能打印出 undefined。
怎么处理这样的陷阱
经过了这么一番的折腾我们必须要来处理它啊。下面给出处理的方法:
同步解决
处理的方法有两种,第一种我们直接改成同步读取文件
var fs = require('fs');
var files = ['a.txt', 'b.txt', 'c.txt'];
var file;
for (var i = 0; i < files.length; i++) {
file = readFileSync(files[i],"utf-8");
console.log(files[i] + ': ' + contents);
}
闭包解决
第二种就是闭包
下面给出具体实现
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);
}
这里用了函数立即执行,将异步外再包裹住一个匿名函数,然后立即执行并传入i,由于回调函数还没执行,i的值不会在匿名函数内被释放,因此,我们便可以轻松的获取到文件名,而不用担心undefined的问题。
其实还有很多种可以绕过陷阱的方法,比如你还可以用一个数组暂时存储住i的值,回调的时候再在里面去取,这样的话,也能够轻松获取到。