fs 是 Node 内置文件系统模块,用于读写文件、操作文件夹。JS 是单线程语言,fs 提供同步阻塞、回调异步、Promise、async/await 四种写法,一步步解决异步嵌套带来的回调地狱问题。
一、模块导入两种方式
- 基础 fs:提供同步 API、回调风格异步 API
js
运行
javascript
import fs from 'fs';
- fs/promises:专门提供 Promise 风格 API,适配 async/await(推荐生产使用)
js
运行
javascript
import fs from 'fs/promises';
二、方案 1:同步读取 readFileSync(阻塞主线程)
特点:代码顺序执行,文件读取期间阻塞整个线程,不适合接口、循环大量 IO;仅适合项目启动一次性读取配置文件。
js
运行
ini
import fs from 'fs';
const syncData = fs.readFileSync('./test.txt', 'utf-8');
console.log(syncData);
缺点:并发场景大量文件读写会严重降低服务性能。
三、方案 2:回调异步 readFile(ES6 原生写法,回调地狱)
Node 统一错误优先回调规范:(err, data) => {}
- 读取成功:err = null,data 为文件内容
- 读取失败:err 为错误对象(文件不存在、权限不足等)
串行读取多个文件时会产生多层嵌套,即回调地狱,维护成本极高。
js
运行
javascript
import fs from 'fs';
fs.readFile('./file1.txt', 'utf-8', (err, data) => {
if (!err) {
console.log('file1', data);
} else {
console.log(err);
}
fs.readFile('./file2.txt', 'utf-8', (err, data) => {
if (!err) {
console.log('file2', data);
} else {
console.log(err);
}
fs.readFile('./file3.txt', 'utf-8', (err, data) => {
if (!err) {
console.log('file3', data);
} else {
console.log(err);
}
})
})
})
四、方案 3:Promise 链式调用(消除嵌套)
基于 fs/promises,readFile 返回 Promise,使用 .then() 链式串行执行,解决多层嵌套问题;缺点是多文件场景重复书写 then,代码冗余。
js
运行
kotlin
import fs from 'fs/promises';
fs.readFile('./file1.txt', 'utf-8')
.then(data => {
console.log('file1', data);
return fs.readFile('./file2.txt', 'utf-8');
})
.then(data => {
console.log('file2', data);
return fs.readFile('./file3.txt', 'utf-8');
})
.then(data => {
console.log('file3', data);
})
五、方案 4:async/await(ES8 最优方案,生产首选)
async/await 是 Promise 的语法糖,底层依旧异步、不会阻塞主线程;代码线性自上而下书写,可读性最强。限制:await 只能写在 async 函数内,顶层直接 await 会报错,使用 IIFE 立即执行函数包裹。补充:Promise、await 属于微任务;setTimeout 属于宏任务。
js
运行
javascript
import fs from 'fs/promises';
(async () => {
const file1Data = await fs.readFile('./file1.txt', 'utf-8');
console.log('file1', file1Data);
const file2Data = await fs.readFile('./file2.txt', 'utf-8');
console.log('file2', file2Data);
const file3Data = await fs.readFile('./file3.txt', 'utf-8');
console.log('file3', file3Data);
})();
完整健壮写法(增加 try/catch 捕获读取错误)
js
运行
javascript
import fs from 'fs/promises';
(async () => {
try {
const data = await fs.readFile('./test.txt', 'utf-8');
console.log(data);
} catch (err) {
console.error('文件读取失败:', err);
}
})();
六、异步代码演进路线
同步阻塞(性能差)→ 回调函数(回调地狱)→ Promise then 链式(冗余)→ async/await(简洁易维护)
七、四种读取方式对比表
表格
| 写法 | 是否阻塞线程 | 优点 | 缺点 |
|---|---|---|---|
| readFileSync | 阻塞 | 写法极简 | 大量 IO 严重影响服务性能 |
| readFile 回调 | 非阻塞 | 无需额外导入 | 串行多文件产生回调地狱 |
| Promise then | 非阻塞 | 无嵌套结构 | 多文件链式代码重复繁琐 |
| async await | 非阻塞 | 线性阅读,代码整洁 | ES8 语法,需 async 函数包裹 |
八、工程开发最佳实践
- 业务文件读写统一使用
fs/promises + async/await; - 所有文件读取必须搭配 try/catch 捕获异常;
- 同步 readFileSync 仅用于项目初始化加载配置,接口业务禁止使用;
- 路径搭配 path.resolve 生成绝对路径,防止运行目录变更导致文件找不到。