You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

异步递归文件系统遍历:回调调用时机及递归完成判定问题

这确实是异步递归遍历目录时很常见的痛点——异步操作的回调嵌套很容易让你摸不清整个遍历流程的结束时机,导致没法准确获取最终的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等待,确保子目录遍历完成后才继续当前目录的后续条目
  • 所有异步操作(readdirstatreadFile)都用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

火山引擎 最新活动