Nodejs 第二十章 fs 上

在 Node.js 中,fs 模块是用来与文件系统进行交互的内置库。这个模块提供了很多功能,允许我们在程序中执行文件操作,如读取文件、写入文件、更改文件权限、操作目录等。这些操作可以是同步的(阻塞)或异步的(非阻塞)

fs多种策略(三种)

异步

  • 首先最常用的异步的选项方式

    • 其中分别有三个选项:
    1. 选项1:我们所要读取的文件路径(文件路径可以是绝对的,也可以是相对于当前工作目录的相对路径)
    2. 选项2:options配置项,这个将放在后面进行总结常用项
    3. 选项3:获取结果的回调函数,其中遵循node规范,第一参数个是err,第二参数是返回的数据(数据是流形式的,可以在options配置项里进行配置或对结果toString)
js 复制代码
const fs = require('node:fs')

// 异步读取文件,第二个选项是options,可以设置编码格式,第三个选项则是回调函数
fs.readFile('xiaoyu01.txt', {}, (err, data) => {
  console.log(data.toString());
})

同步

  • 同步的选项则是在异步的基础上继续后缀加上Sync
    • 这个有一个要素,同步是会阻塞代码 的,也就是当前任务没有执行完,不会执行下一步的内容。只适合读取文件比较小的情况,当读取文件较大最好是使用异步的方式来优化体验
    • 这些同步方法不需要回调函数,它们将直接返回结果或抛出错误
    • 返回的数据和异步一样是buffer的流形式,所以需要进行编码转化,对返回的结果进行toString处理
js 复制代码
const fs = require('node:fs')

const txt = fs.readFileSync('xiaoyu01.txt')
console.log(txt.toString());
console.log("会在读取文件之后执行");

promise

  • promise选项则是fs模块提供的第三种方式,新增的promise版本,只需要在引入包后面增加/promise即可,fs便可支持promise回调
    • 返回的结果以then来进行接收,错误则是用catch进行处理
    • fs返回的是一个buffer二进制数据 每两个十六进制数字表示一个字节
js 复制代码
const fs = require('node:fs/promises')

fs.readFile('./xiaoyu01.txt').then(res=>{
  console.log(res.toString());
}).catch(err=>{
  console.log("抛出错误:",err);
})

常用API 介绍

options配置项

配置项 类型 默认值 描述
encoding stringnull null 指定文件读取的编码,如 'utf8'。如果设为 null,文件内容将作为原始的缓冲区(Buffer)返回。
flag string 'r' 指定文件系统操作的标志。例如,'r' 代表读取文件,如果文件不存在则抛出异常。
  • flag配置型的参数 就很多了,默认值如上表格是r

flag参数配置表

Flag 描述
r 以读取模式打开文件。如果文件不存在,抛出异常。
r+ 以读写模式打开文件。如果文件不存在,抛出异常。
rs+ 以同步读写模式打开文件,绕过本地文件系统缓存。适用于 NFS 挂载等情况。
w 以写入模式打开文件。如果文件不存在则创建文件,如果文件存在则截断为0。
wx w 相似,但如果路径存在,则失败。
w+ 以读写模式打开文件。如果文件不存在则创建文件,如果文件存在则截断为0。
wx+ w+ 相似,但如果路径存在,则失败。
a 以追加模式打开文件。如果文件不存在,则会被创建。
ax a 相似,但如果路径存在,则失败。
a+ 以读取和追加模式打开文件。如果文件不存在,则会被创建。
ax+ a+ 相似,但如果路径存在,则失败。
js 复制代码
import fs2 from 'node:fs/promises'

fs2.readFile('./index.txt',{
    encoding:"utf8",
    flag:"",
}).then(result => {
    console.log(result.toString())
})

fs可读流

在 Node.js 中,fs 模块提供了一个 fs.createReadStream 方法用于创建可读流(Readable Stream),它可以逐块读取大文件而不需要一次性将整个文件内容加载到内存中。这对于处理大型文件或数据流特别有用,因为它有助于降低内存占用并提高应用程序的效率。

创建可读流

js 复制代码
const fs = require('fs');

const readStream = fs.createReadStream('./index.txt',{
    encoding:"utf8"
})

监听事件

可读流对象是 EventEmitter 的实例,你可以用on监听它的事件来处理数据和监控流的状态:

  • data:当有数据可读时触发。
  • end:当整个文件已经读取完毕,流已结束时触发。
  • error:在接收和写入过程中发生错误时触发。
  • close:当流和资源被关闭时触发(并非所有流都会触发此事件)。
js 复制代码
//文件可读时,返回一个chunk(块)
readStream.on('data',(chunk)=>{
    console.log(chunk)
})

//文件读取结束时
readStream.on('end',()=>{
    console.log('close')
})

创建文件夹(目录)

  • 使用 fs.mkdirfs.mkdirSync 创建新的文件夹
    • 如果开启 recursive 可以递归创建多个文件夹
    • mkdir 是 "make directory" 的缩写,其中 "mk" 代表 "make"(创建),"dir" 代表 "directory"(目录)
    • 一般我们就使用Sync同步的方式去创建,因为创建文件夹很快,基本上不存在堵塞的问题
js 复制代码
const fs = require('fs');

// 异步方式
fs.mkdir('./xiaoyu', { recursive: true }, (err) => {
  if (err) throw err;
  console.log('Folder created!');
});

// 同步方式
try {
  //开启 recursive ,我们路径从当下开始递归创建多个文件夹,分别是xiaoyu文件夹以及之内的xiaoman,xiaoman之内的test,嵌套了三层文件夹
  fs.mkdirSync('/xiaoyu/xiaoman/test', { recursive: true });
  console.log('Folder created!');
} catch (err) {
  console.error(err);
}

删除文件夹(目录)

  • 使用 fs.rmdirfs.rmdirSync 删除文件夹。
    • 注意:在删除文件夹之前,该文件夹必须是空的。
    • 如果开启recursive 递归删除全部文件夹
    • rmdir 是 "remove directory" 的缩写,其中 "rm" 代表 "remove"(删除),"dir" 代表 "directory"(目录)
js 复制代码
// 异步方式
fs.rmdir('./xiaoyu', (err) => {
  if (err) throw err;
  console.log('Folder deleted!');
});

// 同步方式
try {
  fs.rmdirSync('/xiaoyu/xiaoman/test');
  console.log('Folder deleted!');
} catch (err) {
  console.error(err);
}

重命名文件夹(目录)

  • 使用 fs.renamefs.renameSync 来重命名或移动文件夹。
js 复制代码
// 异步方式
fs.rename('./xiaoyur', './index', (err) => {
  if (err) throw err;
  console.log('Folder renamed!');
});

// 同步方式
try {
  fs.renameSync('./xiaoyur', './index');
  console.log('Folder renamed!');
} catch (err) {
  console.error(err);
}

监听文件的变化

  • 使用 fs.watch 方法可以监听文件或文件夹的变化。
    • 当文件或文件夹有变化时,回调函数会被调用。eventType 可以是 'rename'(文件重命名) 或 'change'(文件内容更新),并且如果操作系统支持,filename 参数会提供发生变化的文件的名称。
    • 需要注意的是,fs.watch 的行为非常依赖于操作系统,而且在某些情况下可能不稳定(例如,一些系统可能在文件发生变化时不提供 filename)。
    • 对于需要更稳定和一致性的文件监视,建议使用 chokidar 这样的库,它提供了对 fs.watchfs.watchFile 的封装,解决了很多原生 fs 监听功能的问题。
js 复制代码
const fs = require('node:fs')
// filename 是变化的文件的名称
//event返回参数,判断是修改了内容还是修改了文件名
fs.watch('./xiaoyu01.txt', (eventType, filename) => {
  if (eventType === 'change') {
    console.log(`文件发生变化,发生变化的文件名是:${filename}`)
  }
  if (eventType === 'rename') {
    console.log('文件名发生变化')
  }
})

源码解析

  1. C++ 层的 FSReqCallback 这个类
    • Node.js 是用 C++ 和 JavaScript 编写的,其中很多底层操作(如文件系统操作)是由 C++ 部分处理的。在 Node.js 的源代码中,FSReqCallback 是 C++ 代码中的一个类,用于处理与文件系统相关的异步请求。这个类的作用是作为 Node.js 与 libuv 之间的桥梁。
  2. libuvuv_fs_t 的一个封装
    • libuv 是一个跨平台的异步 I/O 库,它提供了 Unix 和 Windows 上的非阻塞 I/O 支持。uv_fs_tlibuv 中用于文件系统操作的结构体。当我们在 Node.js 的 JavaScript 层面调用 fs 模块的函数时,最终会通过 FSReqCallback 类和 uv_fs_t 结构体在 libuv 中执行实际的 I/O 操作。
  3. 将我们 fs 的参数透传给 libuv
    • 当使用 fs 模块进行文件系统操作时,比如读写文件,你传递的参数(如文件路径、编码、回调函数等)会被透传(即直接传递)到 libuv 层。这里的 "透传" 指的是 Node.js 的 JavaScript 层不会修改这些参数,而是直接传递给底层的 C++ 代码和 libuv 库进行处理。

在整个过程中,JavaScript 层的代码调用 C++ 层的代码,C++ 层再调用 libuv,使得 Node.js 能以非阻塞的方式执行文件系统操作。这是 Node.js 异步 I/O 操作的核心,也是它能提供高性能 I/O 操作的关键原因之一。

mkdir 举例

c 复制代码
// 创建目录的异步操作函数,通过uv_fs_mkdir函数调用
// 参数:
// - loop: 事件循环对象,用于处理异步操作
// - req: 文件系统请求对象,用于保存操作的状态和结果
// - path: 要创建的目录的路径
// - mode: 目录的权限模式 777 421
// - cb: 操作完成后的回调函数
int uv_fs_mkdir(uv_loop_t* loop,
                uv_fs_t* req,
                const char* path,
                int mode,
                uv_fs_cb cb) {
  INIT(MKDIR);
  PATH;
  req->mode = mode;
  if (cb != NULL)
    if (uv__iou_fs_mkdir(loop, req))
      return 0;
  POST;
}

注意事项

js 复制代码
const fs = require('node:fs')
//异步方式
//fs 所有的IO操作都是由libuv完成的
fs.readFile('./index.txt', {
    encoding: 'utf-8',
    flag: 'r'
}, (err, dataStr) => {
    if (err) throw err
    console.log('fs')
})

//等本轮事件循环结束执行
//计时器是由v8引擎去完成的
setImmediate(() => {
    console.log('setImmediate')
})
  1. 为什么先走setImmediate 呢,而不是fs?
  2. Node.js 读取文件的时候是使用libuv进行调度的
  3. 而setImmediate是由V8进行调度的
  4. 文件读取完成后 libuv 才会将 fs的结果 推入V8的队列

开始 => fs.readFile请求 => 等待文件IO => 事件循环继续 => 到达poll阶段 => 检查IO操作 => IO未完成 => 进入check阶段 => 执行setImmediate回调 => IO完成 => 文件读取回调加入队列 => 返回到事件循环 => 执行fs.readFile回调 => 结束

  1. 开始:V8 引擎开始执行 JavaScript 代码。
  2. fs.readFile :JavaScript 调用 fs.readFile,这个函数是由 V8 执行的,它将文件读取的请求通过 libuv 库发送出去。
  3. 等待文件IOlibuv 负责处理所有的 I/O 操作,这里它把文件读取操作放入其 I/O 观察队列,并立即返回,让 V8 继续执行 JavaScript 代码。
  4. 事件循环继续 :V8 继续执行后续的 JavaScript 代码,这是在 libuv 管理的事件循环的控制下发生的。
  5. 到达poll阶段libuv 的事件循环到达 poll 阶段,检查文件 I/O 操作是否完成。
  6. 检查IO操作libuv 检查由操作系统处理的 I/O 请求,看是否有完成的操作。
  7. IO未完成 :如果 I/O 操作未完成,libuv 会继续事件循环,检查是否有其他任务如计时器或 setImmediate 待处理。
  8. 进入check阶段libuv 推动事件循环进入 check 阶段。
  9. 执行setImmediate回调 :如果有通过 setImmediate 安排的任务,V8 会在这一阶段执行这些任务的 JavaScript 回调。
  10. IO完成 :一旦 I/O 操作完成,libuv 会将结果放入队列,准备让 V8 执行相应的回调。
  11. 文件读取回调加入队列 :完成的 I/O 操作的回调被 libuv 加入到事件队列中。
  12. 返回到事件循环libuv 继续推动事件循环前进,准备执行更多的任务。
  13. 执行fs.readFile回调 :一旦控制权返回到 V8,它会执行 fs.readFile 的 JavaScript 回调函数。
  14. 结束:所有回调执行完成后,Node.js 程序可能会结束,如果没有其他异步操作待处理。

结论

  • V8 引擎的角色:执行 JavaScript 代码,包括调用异步 API 和运行回调函数。
  • libuv 的角色:管理事件循环,处理所有 I/O 和异步操作,安排回调函数的执行。
  • 二者之间的联系 :V8 提出异步任务请求,libuv 负责执行这些任务并管理何时将任务结果返回给 V8,以便执行相关的 JavaScript 回调。这种设计使得 Node.js 可以非阻塞地处理 I/O,同时保持高性能。
相关推荐
musk121216 分钟前
electron 打包太大 试试 tauri , tauri 安装打包demo
前端·electron·tauri
翻滚吧键盘1 小时前
js代码09
开发语言·javascript·ecmascript
万少1 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL1 小时前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl022 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang2 小时前
前端如何实现电子签名
前端·javascript·html5
海天胜景2 小时前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui
今天又在摸鱼2 小时前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js
蓝婷儿2 小时前
每天一个前端小知识 Day 21 - 浏览器兼容性与 Polyfill 策略
前端
百锦再2 小时前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref