文件IO

I/O:input output

  • 对外部设备的输入输出
  • 外部设备:磁盘、网卡、显卡、打印机、其他…
  • I/O的速度往往远低于内存和CPU的交互速度!

<<现代操作系统>>,英语水平允许的话,建议看原版。中文翻译的。。

fs模块—内置模块

具体用法去官网查api,fs中常用api(下述都是方法名)如下:

  • fs.promises.xxx:node12(ES6之后)之后出现,它会返回一个Promise
    • 后续我们都用这个promises下面的api读写文件等操作 ```javascript const fs = require(“fs”); const path = require(“path”);

const filename = path.resolve(__dirname, “./myfiles/1.txt”); // fs.readFile( // filename, // { // encoding: “utf-8”, //只有encoding配置时,可以直接把这个配置作为readFile的第二个参数 // }, // (err, content) => { // console.log(content); // } // );

// Sync函数是同步的,会导致JS运行阻塞,极其影响性能 // 通常,在程序启动时运行有限的次数即可

// const content = fs.readFileSync(filename, “utf-8”); // console.log(content);

async function test() { const content = await fs.promises.readFile(filename, “utf-8”); console.log(content); }

test();

  1. - fs.readFile:读取一个文件,编码格式默认是**buffer**
  2. - fs.writeFile:向文件写入数据,编码格式默认是**utf-8**。**若目录不存在则会报错,文件不存在会新建**
  3. - **fs.unlink: 删除文件,传一个路径就行**
  4. - fs.copyFile:复制文件内容到另外一个文件
  5. - fs.stat:获取**文件或者目录**的状态信息
  6. - size:占用的字节数。目录的size0,在OS中,目录其实也是一种特殊的空文件,记录一个指针
  7. - birthTime:创建文件的时间戳
  8. - atime: 上次访问文件的时间
  9. - mtime:上次文件内容被修改时间
  10. - ctime:上次文件状态被修改时间
  11. - isDirectory():是否是目录
  12. - isFile():是文件
  13. - 其他很多属性,查api文档
  14. - fs.readdir:获取目录中所有的 **直接** 文件和子目录的名字,由所有字符串组成一个数组
  15. - fs.mkdir:创建目录
  16. - fs.exists:判断文件或目录是否存在,官方把它废弃了。说使用,fs.stat中的那俩属性
  17. <a name="J5DNO"></a>
  18. ## 练习:读取一个目录中的所有子目录和文件
  19. 每个目录或文件都是一个对象,具有一些属性、方法。还有个filename:文件/目录的绝对路径<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1413110/1631873500625-87b2e295-872c-444e-a4bf-bd1ba458e479.png#clientId=u684f05aa-b52b-4&from=paste&height=713&id=u3b3ceef9&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1426&originWidth=1760&originalType=binary&ratio=1&size=1102766&status=done&style=none&taskId=uaf40ea1a-2b49-4fbd-816f-24decf32d40&width=880)
  20. ```javascript
  21. const fs = require("fs");
  22. const path = require("path");
  23. class File {
  24. constructor(filename, name, ext, isFile, size, createTime, updateTime) {
  25. this.filename = filename;
  26. this.name = name;
  27. this.ext = ext;
  28. this.isFile = isFile;
  29. this.size = size;
  30. this.createTime = createTime;
  31. this.updateTime = updateTime;
  32. }
  33. async getContent(isBuffer = false) {
  34. if (this.isFile) {
  35. if (isBuffer) {
  36. return await fs.promises.readFile(this.filename);
  37. } else {
  38. return await fs.promises.readFile(this.filename, "utf-8");
  39. }
  40. }
  41. return null;
  42. }
  43. async getChildren() {
  44. if (this.isFile) {
  45. //文件不可能有子文件
  46. return [];
  47. }
  48. let children = await fs.promises.readdir(this.filename);
  49. children = children.map(name => {
  50. const result = path.resolve(this.filename, name);
  51. return File.getFile(result);
  52. });
  53. return Promise.all(children);
  54. }
  55. static async getFile(filename) {
  56. const stat = await fs.promises.stat(filename);
  57. const name = path.basename(filename);
  58. const ext = path.extname(filename);
  59. const isFile = stat.isFile();
  60. const size = stat.size;
  61. const createTime = new Date(stat.birthtime);
  62. const updateTime = new Date(stat.mtime);
  63. return new File(filename, name, ext, isFile, size, createTime, updateTime);
  64. }
  65. }
  66. async function readDir(dirname) {
  67. const file = await File.getFile(dirname);
  68. return await file.getChildren();
  69. }
  70. async function test() {
  71. const dirname = path.resolve(__dirname, "./myfiles");
  72. const result = await readDir(dirname);
  73. const datas = await result[0].getChildren();
  74. console.log(datas);
  75. }
  76. test();

文件流

什么是流

  1. 流是指数据的流动,数据从一个地方缓缓的、一点点的流动到另外一个地方
  2. 流是有方向的

    1. 可读流:Readable,数据从源头流向内存(比如:文件在磁盘,读文件时,从磁盘流向内存)
    2. 可写流:Writable,数据从内存流向源头(比如:把内存中的数据,写入磁盘中的文件)
    3. 双工流:数据既可从源头流向内存,又可从内存流向源头

      为什么需要流

      时间、空间上的巨大差异!
  3. 其他介质和内存的数据规模不一致

  4. 其他介质和内存的数据处理能力不一致

在node中有个流模块—stream

  1. const { Readable, Writable } = require("stream");//其他类型的流,都应该继承自这俩个

文件流

什么是文件流?—— 内存数据和磁盘文件数据之间的流动
文件流的创建

文件可读流

fs.createReadStream(path[, options])

  1. 含义:创建一个文件可读流,用于读取文件内容
  2. path:要读取的文件路径
  3. options:可选配置
    1. autoClose: 默认为true,文件被读完后就会自动关闭
    2. encoding: 编码方式
    3. start:起始字节
    4. end:结束字节
    5. highWaterMark: 每次读取的数量,默认是64KB,但它的单位不一定是kb,还要看encoding的值
      1. encoding有值,为utf-8(它认为汉字是三个字节)时,highWaterMark为1就代表一个汉字
      2. encoding为null,highWaterMark数量就代表字节数
  4. 返回值:Readable的子类ReadStream,简称rs

    1. 事件:rs.on(‘事件名’, 回调/处理函数)
    2. 事件名有:

      1. open:文件被正常打开后触发回调
      2. error:打开文件失败
      3. close:文件被关闭后就会触发回调
        1. 可通过rs.close();手动关闭,然后就会触发回调
        2. 文件读完了也会自动关闭,然后触发回调
      4. data:读取到一部分数据后就会触发回调,所以会反复触发

        1. 只有注册了这个data事件,才会真正读取数据!否则不读!
        2. 每次读取highWaterMark指定的数量
        3. 回调函数中会附带每次读到的数据
          1. rs.on("data", chunk => {
          2. console.log("读到了一部分数据:", chunk);
          3. rs.pause(); //暂停
          4. rs.resume();//恢复
          5. });
      5. end:全部数据读取完毕后触发回调,先触发end,再触发close

      6. pause:读取数据被暂停时触发回调
        1. re.pause():手动暂停读取,会触发pause事件
      7. resume:重新恢复读取数据时触发回调
        1. rs.resume():手动恢复读取,会触发resume事件

文件可写流

fs.createWriteStream(path[, options])

  1. 含义:创建一个文件可写流,用于向文件写入数据
  2. path:要写入数据的文件路径
  3. options:可选配置
    1. flags:操作文件的方式
      1. w:直接覆盖掉原有内容,a:追加内容
    2. autoClose: 默认为true,数据被写完后就会自动关闭
    3. encoding: 编码方式,默认为utf-8
    4. start:起始字节
    5. highWaterMark: 默认16KB。这里代表的就是每次最多写入的字节数!这点与可读流不一样
      1. 它的大小就是下述写入通道的长度。单位是字节
  4. 返回值:Writable的子类WriteStream,简称ws
    1. ws.on(“事件名”, 处理函数)。用法,基本同可读流
    2. drain事件:写入通道清空时触发回调
    3. ws.write(data); 写入一组数据
      1. data可以是字符串或Buffer
      2. 返回一个boolean值。还是因为内存比硬盘的处理速度快多了
        1. true:写入通道没有被填满,接下来的数据可以直接写入,无须排队
        2. false:写入通道目前已被填满,接下来的数据将进入写入队列

image.png

  1. 3. **背压问题**:写入队列中积压了过多的数据,导致占用内存很高
  2. 1. 当返回false时我们就不写write数据,这样就不会产生背压问题,但是时间依旧节约不了
  3. 1. 利用rs.pipe(ws) 或者 我们自己实现的类似pipe
  4. 4. 写入通道排满了,然后等它清空时(也就是都通道里都写到磁盘里后)!就会触发drain事件!
  1. ws.end([data]); 手动结束写入,然后会自动关闭文件(还取决于autoClose)
    1. data是可选的,表示关闭前最后一次写入的数据 ```javascript const fs = require(“fs”); const path = require(“path”);

const filename = path.resolve(__dirname, “./temp/abc.txt”);

const ws = fs.createWriteStream(filename, { encoding: “utf-8”, highWaterMark: 16 * 1024 });

let i = 0; //一直写,直到到达上限,或无法再直接写入 function write() { let flag = true; while (i < 1024 1024 10 && flag) { flag = ws.write(“a”); //写入a,得到下一次还能不能直接写 i++; } }

write();

ws.on(“drain”, () => { write(); });

  1. <a name="wxT81"></a>
  2. ## 实现:将文件A内容复制到另外一个文件
  3. ```javascript
  4. const fs = require("fs");
  5. const path = require("path");
  6. //方式1, 利用现有api,一次性读写, 占用内存很大
  7. async function method1() {
  8. const from = path.resolve(__dirname, "./temp/abc.txt");
  9. const to = path.resolve(__dirname, "./temp/abc2.txt");
  10. console.time("方式1");
  11. const content = await fs.promises.readFile(from);
  12. await fs.promises.writeFile(to, content);
  13. console.timeEnd("方式1");
  14. console.log("复制完成");
  15. }
  16. //方式2, 读一点写一点,停止、恢复, 其实就是rs.pipe(ws)的原理
  17. async function method2() {
  18. const from = path.resolve(__dirname, "./temp/abc.txt");
  19. const to = path.resolve(__dirname, "./temp/abc2.txt");
  20. console.time("方式2");
  21. const rs = fs.createReadStream(from);
  22. const ws = fs.createWriteStream(to);
  23. rs.on("data", chunk => {
  24. //读到一部分数据
  25. const flag = ws.write(chunk);
  26. if (!flag) {
  27. //表示下一次写入,会造成背压
  28. rs.pause(); //暂停读取
  29. }
  30. });
  31. ws.on("drain", () => {
  32. //可以继续写了
  33. rs.resume();
  34. });
  35. //上述两个on极其内容可以替换为一句: rs.pipe(ws);
  36. rs.on("close", () => {
  37. //写完了
  38. ws.end(); //完毕写入流
  39. console.timeEnd("方式2");
  40. console.log("复制完成");
  41. });
  42. }
  43. method2();

rs.pipe(ws)

  1. 将可读流连接到可写流
  2. 返回值就是ws
  3. 该方法可解决背压问题