在 Node.js 中,fs
模块是用来与文件系统进行交互的内置库。这个模块提供了很多功能,允许我们在程序中执行文件操作,如读取文件、写入文件、更改文件权限、操作目录等。这些操作可以是同步的(阻塞)或异步的(非阻塞)
fs多种策略(三种)
异步
-
首先最常用的
异步
的选项方式- 其中分别有三个选项:
- 选项1:我们所要读取的文件路径(文件路径可以是绝对的,也可以是相对于当前工作目录的相对路径)
- 选项2:options配置项,这个将放在后面进行总结常用项
- 选项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 |
string 或 null |
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.mkdir
或fs.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.rmdir
或fs.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.rename
或fs.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.watch
和fs.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('文件名发生变化')
}
})
源码解析
C++
层的FSReqCallback
这个类- Node.js 是用 C++ 和 JavaScript 编写的,其中很多底层操作(如文件系统操作)是由 C++ 部分处理的。在 Node.js 的源代码中,
FSReqCallback
是 C++ 代码中的一个类,用于处理与文件系统相关的异步请求。这个类的作用是作为 Node.js 与libuv
之间的桥梁。
- Node.js 是用 C++ 和 JavaScript 编写的,其中很多底层操作(如文件系统操作)是由 C++ 部分处理的。在 Node.js 的源代码中,
- 对
libuv
的uv_fs_t
的一个封装libuv
是一个跨平台的异步 I/O 库,它提供了 Unix 和 Windows 上的非阻塞 I/O 支持。uv_fs_t
是libuv
中用于文件系统操作的结构体。当我们在 Node.js 的 JavaScript 层面调用fs
模块的函数时,最终会通过FSReqCallback
类和uv_fs_t
结构体在libuv
中执行实际的 I/O 操作。
- 将我们
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')
})
- 为什么先走setImmediate 呢,而不是fs?
- Node.js 读取文件的时候是使用libuv进行调度的
- 而setImmediate是由V8进行调度的
- 文件读取完成后 libuv 才会将 fs的结果 推入V8的队列
开始 => fs.readFile请求 => 等待文件IO => 事件循环继续 => 到达poll阶段 => 检查IO操作 => IO未完成 => 进入check阶段 => 执行setImmediate回调 => IO完成 => 文件读取回调加入队列 => 返回到事件循环 => 执行fs.readFile回调 => 结束
- 开始:V8 引擎开始执行 JavaScript 代码。
fs.readFile
:JavaScript 调用fs.readFile
,这个函数是由 V8 执行的,它将文件读取的请求通过libuv
库发送出去。- 等待文件IO :
libuv
负责处理所有的 I/O 操作,这里它把文件读取操作放入其 I/O 观察队列,并立即返回,让 V8 继续执行 JavaScript 代码。 - 事件循环继续 :V8 继续执行后续的 JavaScript 代码,这是在
libuv
管理的事件循环的控制下发生的。 - 到达poll阶段 :
libuv
的事件循环到达 poll 阶段,检查文件 I/O 操作是否完成。 - 检查IO操作 :
libuv
检查由操作系统处理的 I/O 请求,看是否有完成的操作。 - IO未完成 :如果 I/O 操作未完成,
libuv
会继续事件循环,检查是否有其他任务如计时器或setImmediate
待处理。 - 进入check阶段 :
libuv
推动事件循环进入 check 阶段。 - 执行setImmediate回调 :如果有通过
setImmediate
安排的任务,V8 会在这一阶段执行这些任务的 JavaScript 回调。 - IO完成 :一旦 I/O 操作完成,
libuv
会将结果放入队列,准备让 V8 执行相应的回调。 - 文件读取回调加入队列 :完成的 I/O 操作的回调被
libuv
加入到事件队列中。 - 返回到事件循环 :
libuv
继续推动事件循环前进,准备执行更多的任务。 - 执行fs.readFile回调 :一旦控制权返回到 V8,它会执行
fs.readFile
的 JavaScript 回调函数。 - 结束:所有回调执行完成后,Node.js 程序可能会结束,如果没有其他异步操作待处理。
结论
- V8 引擎的角色:执行 JavaScript 代码,包括调用异步 API 和运行回调函数。
- libuv 的角色:管理事件循环,处理所有 I/O 和异步操作,安排回调函数的执行。
- 二者之间的联系 :V8 提出异步任务请求,
libuv
负责执行这些任务并管理何时将任务结果返回给 V8,以便执行相关的 JavaScript 回调。这种设计使得 Node.js 可以非阻塞地处理 I/O,同时保持高性能。