前言:参考在Node中,Buffer和Stream无处不在。Buffer是缓冲区的意思,Stream是流的意思。在计算机中,缓冲区是存放中间变量的存储区,方便CPU读取数据;流类比水流来描述数据的流动。Buffer和Stream一般都是字节级别的操作。binarybufferbuffer在前端,我们只需要做字符串级别的操作,很少接触bytes、bases等低级操作。一方面,这足以满足日常需求。另一方面,Javascript这种应用层语言并没有做到这一点。但是在后端,处理文件、网络协议、图片、视频等是很常见的,尤其是文件、网络流等操作处理二进制数据时。为了让javascript能够处理二进制数据,node封装了一个Buffer类,主要用来操作字节和处理二进制数据。//创建一个长度为10并填充30的缓冲区。constbuf1=Buffer.alloc(10,30)console.log(buf1)////StringtoBufferconstbuf2=Buffer.from('javascript')控制台。log(buf2)////字符串到bufferconsole.log(buf2.toString())//javascriptconsole.log(buf2.toString('hex'))//6a617661736372697074ABuffer类似于一个整数数组,可以下标,有长度属性,有剪切和复制等操作,很多API也类似于数组,但是Buffer的大小在创建时就确定了,不能进行调整。Buffer处理的是字节,两位十六进制,所以整数的范围是0-255。可见Buffer和string可以相互转换,还可以设置字符集编码。Buffer用于处理文件I/O和网络I/O传输的二进制数据,string用于表示。在处理文件I/O和网络I/O传输的二进制数据时,尽量直接以Buffer的形式传输,速度会大大提高,但是操作字符串还是比操作Buffer快很多.Buffer内存分配和性能优化:Buffer是javascript和C++结合的典型模块。性能相关的功能用C++实现,javascript负责连接和提供接口。Buffer占用的内存不是V8分配的。它独立于V8堆内存。内存申请和JavaScript分配是通过C++层面实现的。值得一提的是,每当我们使用Buffer.alloc(size)申请一块Buffer内存时,Buffer都会以8KB为边界判断是大对象还是小对象,小对象存放在剩余内存中pool,如果不够,再申请一个8KB的内存池;大对象直接使用在C++级别请求的内存。所以,对于一个大的对象来说,申请一块大的内存要比申请很多小的内存池要快的多。StreamStream如前所述,流类似于水流来描述数据的流动。文件I/O和网络I/O中数据的传输都可以称为流。流是可以统一描述所有常见输入输出类型的模型。读取和写入字节序列的抽象表示。数据从A端到B端的流向不同于B端到A端的数据流向,因此具有方向性。A端向B端输入数据,B为输入流,获取的对象为可读流;对于A,它是输出端,得到的对象是一个可写流。有些流是可以读写的,比如TCP连接、Socket连接等,称为读写流(Duplex)。还有一种读写流,可以在读写过程中对数据进行修改和转换,称为Transformstream。在node中,这些流中的数据都是Buffer对象。可读可写流会将数据存储在内部缓存中,等待被消费;Duplex和Transform都维护了两个独立的缓存用于读写。在保持合理高效的数据流的同时,还可以让读写独立进行,互不影响。在node中,这四个流都是EventEmitter的实例,它们都有close和error事件,可读流有数据事件监听数据到达等,可写流有finish事件监听数据已经传递给下层级系统等,Duplex和Transform都实现了Readable和Writable的事件和接口。值得一提的是writable的drain事件,这个事件表示缓存的数据已经被清空。为什么要举办这个活动?原因是调用可写流的write和可读流的read都会有一个缓冲区来缓存写入/读取的数据。缓冲区有大小。一旦写入的内容超过这个大小,write方法就会返回false。表示写入已停止。此时如果继续读取缓冲区中的数据,缓冲区被清空,就会触发drain事件。这样做可以防止缓冲区爆裂:varrs=fs.createReadStream(src);varws=fs.createWriteStream(dst);rs.on('data',function(chunk){if(ws.write(chunk)===false){rs.pause();}});rs.on('结束',函数(){ws.end();});ws.on('drain',function(){rs.resume();});一些常见的流分类:可写流:HTTP请求,客户端上,HTTP响应,服务器上,fs写流,zlib流,加密流,TCP套接字,子进程stdin,process.stdout,process.stderr可读流:HTTP响应,客户端上,HTTP请求,服务器上,fs读取流,zlib流,加密流,TCP套接字,子进程stdout和stderr,process.stdin可读和可写流:TCP套接字,zlib流,加密流转换流:zlibstreams,cryptostreams不得不提管道的概念,也很形象:水从一端流到另一端需要管道作为通道或介质。流也是如此,终端之间数据的传输也需要管道。在node中,是这样的://将readable中的所有数据通过pipelines传递给一个名为file.txt的文件constreadable=getReadableStreamSomehow();constwritable=getWritableStreamSomehow('file.txt');//将readable中的所有数据传递给'file.txt'readable.pipe(writable);//流上的链式管道操作constr=fs.createReadStream('文件.txt');constz=zlib.createGzip();constw=fs.createWriteStream('file.txt.gz');r.pipe(z).pipe(w);请注意,只有可读流具有管道功能,可写流才能用作目标。管道不仅可以作为通道使用,还可以很好地控制管道内的流量,控制读写的平衡,防止任何一方过度操作。另外,管道可以监听可读流的数据和结束事件,从而建立快速响应://一个文件下载的例子,如果使用回调函数,需要等到服务器有在将数据发送到浏览器之前完成读取文件varhttp=require('http');varfs=require('fs');varserver=http.createServer(function(req,res){fs.readFile(__dirname+'/data.txt',function(err,data){res.end(data);});});server.listen(8888);//使用stream方法,只要建立连接,数据将在不等待服务器完成缓存data.txtvarhttp=require('http')varfs=require('fs')varserver=http.createServer(function(req,res){varstream=fs.createReadStream(__dirname+'/data.txt')stream.pipe(res)})server.listen(8888)所以使用pipe可以解决上面的清算问题。fs文件模块fs文件模块是一个高层模块,继承了EventEmitter、stream、path等底层模块,提供了对文件的操作,包括读、写、重命名、删除、遍历目录、链接POSIX文件系统、等操作。不同于node设计思路和其他模块,fs模块中的所有操作都提供了异步和同步两种版本。fs模块主要由以下几部分组成:对底层POSIX文件系统的封装,对应操作系统原生的文件操作,继承Stream的文件流fs.createReadStream和fs.createWriteStream同步文件操作方法,如fs.readFileSync,fs.writeFileSync异步文件操作方法,fs.readFile和fs.writeFile读写操作:constfs=require('fs');//导入fs模块/*读取文件*///使用流constread=fs.createReadStream('sam.js',{encoding:'utf8'});read.on('data',(str)=>{console.log(str);})//使用readFilefs.readFile('test.txt',{},function(err,data){if(err){throwerr;}console.log(data);});//open+readfs.open('test.txt','r',(err,fd)=>{fs.fstat(fd,(err,stat)=>{varlen=stat.size;//检测文件长度varbuf=newBuffer(len);fs.read(fd,buf,0,len,0,(err,bw,buf)=>{console.log(buf.toString('utf8'));fs.close(fd);})});});/*写文件和读文件API形式类似于*/读/写文件,也有3种方式,那么有什么区别呢?createReadStream/createWriteStream创建一个ReadStream对象,它将文件内容读取为流数据。该方法的主要目的是将数据读入流中,得到一个可读流,方便用流进行操作readFile/writeFile:Node.js将文件的内容看成一个整体,一块缓存区分配给它并且文件的内容是一次性读/写的在此期间,Node.js将无法进行任何其他处理,因此在读写大文件时,可能会导致缓存“爆炸”read/writeread/write文件内容不断是一小块内容在将文件读/写到缓存区,最后从缓存区读取文件内容。同步API也是如此。最常用的是readFile,用于读取大文件。read提供了更多细节,底层操作,read必须配合open。获取文件状态:fs.stat('eda.txt',(err,stat)=>{if(err)throwerrconsole.log(stat)})/*Stats{dev:16777220,mode:33279,nlink:1,uid:501,gid:20,rdev:0,blksize:4194304,ino:4298136825,size:0,blocks:0,atimeMs:1510317983760.94,-文件数据的最后访问时间mtimeMs:1510317983760.94,-最近修改的时间。ctimeMs:1510317983777.8538,-文件状态最后一次改变的时间birthtimeMs:1509537398000,atime:2017-11-10T12:46:23.761Z,mtime:2017-11-10T12:46:23.761Z,ctime:2017-121:46:23.778Z,出生时间:2017-11-01T11:56:38.000Z}*/观察文件:constFSWatcher=fs.watch('eda.txt',(eventType,filename)=>{console.log(`${eventType}`)})FSWatcher.on('change',(eventType,filename)=>{console.log(`${filename}`)})//watch和返回的FSWatcher实例的回调函数绑定在fs.watchFile('message.text',(curr,prev)=>{console.log(`当前mtime是:${curr.mtime}`);console.log(`前一个mtime是:${prev.mtime}`);})监控文件还是有两种方式:watch调用底层API监控文件,快速可靠。watchFile是通过不断轮询fs.Stat(fileStatisticaldata)来获取被监控文件的变化,速度较慢,可靠性较差。另外,回调函数的参数是fs.Stat实例,所以尽量使用watch。watchFile用于需要更多文件信息的场景。其他的创建,删除,复制,移动,重命名,检查文件,修改权限...总结从Buffer到Stream,再到fs文件模块,串联起来可以对整个知识有更清晰的认识,也有一个对webpack、gulp等前端自动化工具有了更清晰的认识,对工作流构建的机制和实现有了更深入的了解。学习其他知识也是如此——知道来龙去脉,知道为什么存在,知道它们之间的联系,就可以把零散的知识联系起来。