一个“灵异”的文件夹遍历
凌晨2点,程序员小张盯着屏幕上的代码陷入沉思——他只是想遍历文件夹,统计所有子目录数量。但无论怎么调试,输出结果总是随机变化:有时是3个,有时是5个,甚至偶尔是0。他抓狂地发了一条朋友圈:“Node的异步是玄学吗?”
半小时后,资深架构师老王回复:“不是玄学,是你把异步当同步用了。”
Node异步转同步的常见误区
1. 在循环中直接调用异步函数
// 错误!files.forEach中的回调无法保证顺序执行
fs.readdir("./data", (err, files) => {
files.forEach(file => {
fs.stat(file, (err, stats) => {
if (stats.isDirectory()) count++;
});
});
console.log(count); // 永远输出0!
});
forEach
循环中的异步回调无法阻塞后续代码,导致console.log
在回调完成前执行。
正确解法1:async/await + Promise.all
const fs = require('fs').promises;
async function countFolders() {
const files = await fs.readdir("./data");
let count = 0;
await Promise.all(files.map(async file => {
const stats = await fs.stat(file);
if (stats.isDirectory()) count++;
}));
console.log(count); // 正确输出
}
正确解法2:for…of 顺序执行
async function countFoldersSafe() {
const files = await fs.readdir("./data");
let count = 0;
for (const file of files) {
const stats = await fs.stat(file);
if (stats.isDirectory()) count++;
}
console.log(count); // 正确输出
}
2. 忽略错误边界,导致“静默崩溃”
// 错误!未捕获Promise异常,进程可能崩溃
async function readFiles() {
const data = await fs.promises.readFile("a.txt");
// 假设data格式错误,下方代码抛出异常
JSON.parse(data);
}
readFiles(); // 异常未被捕获!
未用try/catch
包裹异步操作,进程可能因未处理错误直接退出!
正确解法:try/catch包裹 + 全局错误监听双重保障
// try/catch包裹
async function readFilesSafe() {
try {
const data = await fs.promises.readFile("a.txt");
JSON.parse(data);
} catch (err) {
console.error('文件处理失败:', err); // 捕获所有错误
}
}
// 全局错误监听(放在入口文件)
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason);
});
3. 滥用同步API,拖垮性能
// 错误!用同步API处理大文件,阻塞事件循环
const content = fs.readFileSync("large.log"); // 卡住整个进程!
同步API会阻塞事件循环,高并发场景下性能暴跌
正确解法1:流式处理(内存友好)
const fs = require('fs');
const readStream = fs.createReadStream('large.log');
readStream
.on('data', chunk => {
// 处理数据块
})
.on('end', () => {
console.log('文件读取完成');
});
正确解法2:异步API + 并发控制
const pLimit = require('p-limit');
const limit = pLimit(3); // 最大并发数3
async function processFiles(files) {
await Promise.all(files.map(file =>
limit(async () => {
const data = await fs.promises.readFile(file);
// 处理文件
})
));
}
避坑指南:写出健壮的异步转同步代码
1. 优先使用现代语法
- async/await
用同步思维写异步代码,配合Promise.all
处理并行任务。 - util.promisify
将回调函数快速转为Promise。
// util.promisify转换回调函数
const util = require('util');
const fs = require('fs');
const readFileAsync = util.promisify(fs.readFile);
2. 复杂场景用工具库
- Async.js
提供series
(串行)、parallel
(并行)等流程控制 - RxJS
通过响应式编程优雅处理异步流
async.series([
callback => fs.readFile('a.txt', callback),
callback => fs.readFile('b.txt', callback)
], (err, results) => {
// 顺序处理结果
});
3. 性能与可读性的平衡
- 避免过度同步化
Node的异步模型是为高并发设计,强行同步可能适得其反 - 监控事件循环延迟
用process.hrtime()
检测阻塞
异步不是敌人,误解才是
Node的异步模型是其高性能的核心,误用同步化如同给跑车装上马车轮。理解事件循环、善用Promise
与async/await
、严格处理错误边界,才能让异步代码既高效又可靠。记住:代码是写给人看的,偶尔才是给机器执行的。
🔥 关注我的公众号「哈希茶馆」一起交流更多开发技巧