异步递归文件系统遍历:回调调用时机及递归完成判定问题
这确实是异步递归遍历目录时很常见的痛点——异步操作的回调嵌套很容易让你摸不清整个遍历流程的结束时机,导致没法准确获取最终的nodes数组。我来给你拆解下解决方案,两种思路都给你讲讲,优先推荐更清晰的Promise+async/await方案:
方案一:用Promise + Async/Await 简化异步流程
这是现在最推荐的方式,能把异步代码写得像同步逻辑一样,完美解决递归结束时机的问题。Node.js的fs模块已经内置了Promise化的API(fs.promises),不用自己手动封装回调:
const fs = require('fs').promises; const path = require('path'); // 用来存储最终提取数据的数组 const nodes = []; // 递归遍历目录的异步函数 async function traverseDirectory(dirPath) { // 读取当前目录下的所有文件/子目录 const entries = await fs.readdir(dirPath); // 逐个处理目录下的条目 for (const entry of entries) { const fullPath = path.join(dirPath, entry); // 获取条目元信息,判断是文件还是目录 const stats = await fs.stat(fullPath); if (stats.isDirectory()) { // 如果是子目录,递归调用并等待遍历完成 await traverseDirectory(fullPath); } else { // 匹配你需要提取数据的特定文件(这里示例用后缀判断,可根据需求修改) if (entry.endsWith('.json')) { // 读取文件内容并解析 const fileContent = await fs.readFile(fullPath, 'utf8'); const data = JSON.parse(fileContent); // 将提取的数据加入数组 nodes.push(data); } } } } // 主入口函数,启动遍历并等待全部完成 async function main() { try { // 替换成你的目标根目录路径 await traverseDirectory('./your-root-folder'); // 遍历完成!现在可以安全访问nodes数组了 console.log('所有数据提取完成:', nodes); // 这里可以做后续处理,比如写入文件、返回给接口等 } catch (err) { console.error('遍历过程出错:', err); } } main();
关键说明
traverseDirectory是异步函数,每次递归调用都用await等待,确保子目录遍历完成后才继续当前目录的后续条目- 所有异步操作(
readdir、stat、readFile)都用Promise化的API,彻底避免回调地狱 - 当
main函数里的await traverseDirectory(...)执行完成时,整个目录树的遍历和数据提取就全部结束了,此时nodes数组已经填充好所有目标数据
方案二:传统回调方式(用计数器跟踪任务完成)
如果因为环境限制不能用async/await,那可以用“任务计数器”的方式来跟踪所有异步操作的完成情况:
const fs = require('fs'); const path = require('path'); const nodes = []; let pendingTasks = 0; function traverseDirectory(dirPath, finishCallback) { pendingTasks++; // 启动目录读取任务,计数器+1 fs.readdir(dirPath, (err, entries) => { if (err) { pendingTasks--; return finishCallback(err); } if (entries.length === 0) { pendingTasks--; // 检查是否所有任务都已完成 if (pendingTasks === 0) finishCallback(null, nodes); return; } entries.forEach(entry => { const fullPath = path.join(dirPath, entry); pendingTasks++; // 启动条目元信息查询任务,计数器+1 fs.stat(fullPath, (err, stats) => { if (err) { pendingTasks--; return finishCallback(err); } if (stats.isDirectory()) { // 递归遍历子目录 traverseDirectory(fullPath, finishCallback); } else { if (entry.endsWith('.json')) { pendingTasks++; // 启动文件读取任务,计数器+1 fs.readFile(fullPath, 'utf8', (err, content) => { if (err) { pendingTasks--; return finishCallback(err); } const data = JSON.parse(content); nodes.push(data); pendingTasks--; // 检查是否所有任务都已完成 if (pendingTasks === 0) finishCallback(null, nodes); }); } else { pendingTasks--; if (pendingTasks === 0) finishCallback(null, nodes); } } pendingTasks--; // 条目元信息查询完成,计数器-1 if (pendingTasks === 0) finishCallback(null, nodes); }); }); pendingTasks--; // 目录读取完成,计数器-1 if (pendingTasks === 0) finishCallback(null, nodes); }); } // 启动遍历 traverseDirectory('./your-root-folder', (err, result) => { if (err) { console.error('遍历出错:', err); return; } // 所有遍历和数据提取完成,result就是最终的nodes数组 console.log('数据提取完成:', result); });
关键说明
- 用
pendingTasks计数器跟踪所有未完成的异步操作,每启动一个任务就+1,操作完成后就-1 - 每次异步操作结束后,都检查计数器是否为0,如果是,就触发最终的回调函数,此时
nodes数组已经准备好 - 这种方式需要仔细管理计数器的增减,很容易漏加漏减,所以优先推荐方案一
内容的提问来源于stack exchange,提问作者Mayur Arora




