应该是最后一篇了,因为种种,作者决定下一段实习去北京
💼 需求背景
场景:需要判断一个文件的存在情况,根据文件存在情况判断后续操作.
一种错觉还是现在的我还不能明悟,总感觉大家在js中习惯使用异步方法(也包括我自己), 平平无奇的某一天班,同事处理文件操作需求的时候,并没有达到理想状况, 检查之后才发现,原来是有一个异步方法的处理出现了错误---发生了竞态
竞态条件指的是:程序的输出或行为依赖于事件或线程无法控制的时序。
小明曾经在MDN中,看到过网络请求场景中的竞态问题(HTTP 条件请求 - HTTP | MDN)
在当初的工作需求场景中,它是这样发生的:
- 时刻 T1: 你的代码使用
fs.exists()或fs.access()检查文件/path/to/file是否存在。 - 时刻 T2: 检查返回
true,文件存在。你的程序决定进行下一步操作(比如读取文件)。 - 时刻 T3: 在你调用
fs.readFile()之前的极短瞬间,由于某些原因(例如另一个进程、另一个异步操作),这个文件被删除或修改了。 - 时刻 T4: 你的
fs.readFile()开始执行,但它会立即失败,因为它在 T3 时刻所基于的状态(文件存在)已经失效了。
关键问题在于:exists/access的检查结果只是一个 “快照” ,它只在检查的那个瞬间是准确的。在异步的“检查”和“操作”之间,文件系统的状态可能已经改变。
错误的模式:检查然后操作(Check-then-Act)
简单用代码举个例子,
const fs = require('fs');
const path = '/path/to/file';
// ❌ 错误的做法:存在竞态条件
fs.access(path, fs.constants.F_OK, (err) => {
if (err) {
console.log('文件不存在');
return;
}
// 在这个时间点,文件可能被删除了!
fs.readFile(path, 'utf8', (err, data) => {
if (err) {
// 这里可能会意外出错,例如 ENOENT (文件不存在)
console.error('读取文件出错:', err);
return;
}
console.log(data);
});
});
使用 fs.promises的 async/await语法,这个错误看起来更“自然”(上面说的场景,当时也是这种方式),但本质一样:
import fs from 'fs/promises';
// ❌ 同样是错误的做法:依然存在竞态条件
async function readFileSafely(filePath) {
try {
await fs.access(filePath); // 检查
// 检查后,操作前,文件可能被删除
const data = await fs.readFile(filePath, 'utf8'); // 操作
return data;
} catch (err) {
console.error('操作失败:', err);
}
}
没什么好说的,大家尽可能避免这种问题就好,推荐下面这种
正确的解决方案:直接操作,处理错误(Just Do It, Handle Errors)
1. 回调函数风格的正确做法
const fs = require('fs');
// ✅ 正确的做法:直接操作,处理错误
fs.readFile('/path/to/file', 'utf8', (err, data) => {
if (err) {
// 在这里统一处理所有可能的错误
if (err.code === 'ENOENT') {
console.log('文件不存在');
} else if (err.code === 'EACCES') {
console.log('权限不足');
} else {
console.error('其他错误:', err);
}
return;
}
// 成功读取数据
console.log(data);
});
2. Promise/async-await 风格的正确做法
import fs from 'fs/promises';
// ✅ 正确的做法:直接操作,处理错误
async function readFileSafely(filePath) {
try {
const data = await fs.readFile(filePath, 'utf8'); // 直接操作
return data;
} catch (err) {
// 在这里统一处理所有可能的错误
if (err.code === 'ENOENT') {
console.log('文件不存在');
} else if (err.code === 'EACCES') {
console.log('权限不足');
} else {
console.error('其他错误:', err);
}
// 可以选择重新抛出错误,或者返回一个默认值
// throw err;
return null;
}
}
又叫小明学到了,
后面也为此去看了很多大佬博客,忘记具体是哪篇文章了,当中提到:
Node.js 文件系统操作的设计哲学就是:直接进行你最终想要的操作,如果出错,再通过错误对象来处理具体的原因。
这是因为文件操作本身(如 fs.readFile, fs.writeFile)在内部已经包含了必要的检查。你应该相信这些 API,并让它们给你一个统一的错误处理入口.
站在现在的时间点,回望我3个月的实习期,其中还有一些需求场景用到了Tampermonkey(篡改猴/油猴)插件, 当初用ai直接产出,尽在意最终的需求实现,以后准备单独写个专栏,记录学习Tampermonkey.
小明就要开启下一段故事了(实习),
“虽然要开启新旅程啦,但会一直记得在老东家和各位一起奋斗的日子!祝mt(佩奇)和同事们吃好喝好没烦恼,业绩健康都满分,继续保持联系哦~”