NodeJS:关于异步“readdir”和“stat”的混淆

3
在文档中展示了 readdirstat 的两个版本。它们都有异步和同步版本 readir/readdirSyncstat/statSync
因为 readidirstat 是异步的,我希望它们能返回一个 Promise。但是,在尝试使用 async/await 时,脚本不会等待 readdir 解决,如果我使用 .then/.catch,则会出现错误 cannot read .then of undefined
我想做的就是将运行脚本所在目录内存在的目录映射到 dirsOfCurrentDir 映射表中。

返回错误 cannot read .then of undefined

const fs = require('fs');

const directory = `${ __dirname }/${ process.argv[2] }`;
const dirsOfCurrentDir = new Map();

fs.readdir(directory, (err, files) => {
  let path;

  if (err)
    return console.log(err);

  files.forEach(file => {
    path = directory + file;

    fs.stat(path, (err, stats) => {
      if (err)
        return console.log(err);

      dirsOfCurrentDir.set(file, directory);
    });
  });
}).then(() => console.log('adasdasd'))

console.log(dirsOfCurrentDir)

返回 Map {}

const foo = async () => {
  await fs.readdir(directory, (err, files) => {
    let path;

    if (err)
      return console.log(err);

    files.forEach(file => {
      path = directory + file;

      fs.stat(path, (err, stats) => {
        if (err)
          return console.log(err);

        dirsOfCurrentDir.set(file, directory);
      });
    });
  });
};

foo()
console.log(dirsOfCurrentDir)

编辑

我最终选择使用同步版本的这两个函数readdirSyncstatSync。虽然我更愿意使用异步方法或promisify,但我仍然没有弄清楚如何使用它们使我的代码正常工作。

const fs = require('fs');

const directory = `${ __dirname }/${ process.argv[2] }`;
const dirsOfCurrentDir = new Map();

const dirContents = fs.readdirSync(directory);

dirContents.forEach(file => {
  const path = directory + file;
  const stats = fs.statSync(path);

  if (stats.isDirectory())
    dirsOfCurrentDir.set(file, path);
});

console.log(dirsOfCurrentDir); // logs out the map with all properties set

readdircallback 参数是您将传递到 .then 中的函数。它不像文档所示返回一个 Promise。 - pushkin
1
也许你应该看一下这个链接:https://dev59.com/fVcP5IYBdhLWcg3wr70F - crellee
1
@BrandonBenefield它并没有明确说明它不返回任何内容,但如果它返回了某些内容,它会明确说明(例如“返回一个Promise”)。由于它没有这样说,所以你不能假设它返回一个promise。 - pushkin
1
在Stack Overflow上,您不应该将答案添加到问题中。问题是用来提问的,答案是用来回答问题的。两者不应混淆。如果您想要添加自己的答案到自己的问题中,可以这样做。请从您的问题中删除解决方案。它不属于那里。 - jfriend00
1
如果这是服务器端代码,除了在启动时使用同步I/O之外,在任何其他地方使用同步I/O都会对服务器的可扩展性造成灾难性影响。它会直接导致可扩展性的死亡。 - jfriend00
显示剩余10条评论
1个回答

7

由于readidir和stat是异步的,我希望它们能够返回一个Promise。

首先,请确保您知道异步函数和async函数之间的区别。在JavaScript中使用特定关键字声明为async的函数:

async function foo() {
    ...
}

async 关键字定义的函数总是返回 Promise(根据 Promise 定义)。

但像 fs.readdir() 这样的异步函数可能会返回 Promise,也可能不会,这取决于其内部设计。在这种特定情况下,node.js 中的 fs 模块的原始实现仅使用回调函数而非 Promise(它的设计早于 node.js 中的 Promise 存在)。它的函数是异步的,但没有声明为 async,因此它使用的是常规回调函数而非 Promise。

因此,您必须使用回调函数或 "promisify" 接口将其转换为返回 Promise 的形式,以便您可以使用 await

在 node.js v10 中有一个实验性接口,为 fs 模块提供了内置的 Promise 支持。

const fsp = require('fs').promises;

fsp.readdir(...).then(...)

在早期版本的 Node.js 中,有许多用于将函数转换为 Promise 的选项。您可以使用 util.promisify() 来逐个将函数转换成 Promise:

const promisify = require('util').promisify;
const readdirP = promisify(fs.readdir);
const statP = promisify(fs.stat);

因为我还没有在node v10上进行开发,所以我经常使用Bluebird promise库并一次性将整个fs库promisify:

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));

fs.readdirAsync(...).then(...)

仅列出给定目录中的子目录,您可以执行以下操作:
const fs = require('fs');
const path = require('path');
const promisify = require('util').promisify;
const readdirP = promisify(fs.readdir);
const statP = promisify(fs.stat);

const root = path.join(__dirname, process.argv[2]);

// utility function for sequencing through an array asynchronously
function sequence(arr, fn) {
    return arr.reduce((p, item) => {
        return p.then(() => {
            return fn(item);
        });
    }, Promise.resolve());
}

function listDirs(rootDir) {
    const dirsOfCurrentDir = new Map();
    return readdirP(rootDir).then(files => {
        return sequence(files, f => {
            let fullPath = path.join(rootDir, f);
            return statP(fullPath).then(stats => {
                if (stats.isDirectory()) {
                    dirsOfCurrentDir.set(f, rootDir)
                }
            });
        });
    }).then(() => {
        return dirsOfCurrentDir;
    });  
}

listDirs(root).then(m => {
    for (let [f, dir] of m) {
        console.log(f);
    }
});

这里是一个更通用的实现,列出文件并提供多种选项来指定列表和呈现结果的方式:

const fs = require('fs');
const path = require('path');
const promisify = require('util').promisify;
const readdirP = promisify(fs.readdir);
const statP = promisify(fs.stat);

const root = path.join(__dirname, process.argv[2]);

// options takes the following:
//     recurse: true | false - set to true if you want to recurse into directories (default false)
//     includeDirs: true | false - set to true if you want directory names in the array of results
//     sort: true | false - set to true if you want filenames sorted in alpha order
//     results: can have any one of the following values
//              "arrayOfFilePaths" - return an array of full file path strings for files only (no directories included in results)
//              "arrayOfObjects" - return an array of objects {filename: "foo.html", rootdir: "//root/whatever", full: "//root/whatever/foo.html"}

// results are breadth first

// utility function for sequencing through an array asynchronously
function sequence(arr, fn) {
    return arr.reduce((p, item) => {
        return p.then(() => {
            return fn(item);
        });
    }, Promise.resolve());
}

function listFiles(rootDir, opts = {}, results = []) {
    let options = Object.assign({recurse: false, results: "arrayOfFilePaths", includeDirs: false, sort: false}, opts);

    function runFiles(rootDir, options, results) {
        return readdirP(rootDir).then(files => {
            let localDirs = [];
            if (options.sort) {
                files.sort();
            }
            return sequence(files, fname => {
                let fullPath = path.join(rootDir, fname);
                return statP(fullPath).then(stats => {
                    // if directory, save it until after the files so the resulting array is breadth first
                    if (stats.isDirectory()) {
                        localDirs.push({name: fname, root: rootDir, full: fullPath, isDir: true});
                    } else {
                        results.push({name: fname, root: rootDir, full: fullPath, isDir: false});
                    }
                });
            }).then(() => {
                // now process directories
                if (options.recurse) {
                    return sequence(localDirs, obj => {
                        // add directory to results in place right before its files
                        if (options.includeDirs) {
                            results.push(obj);
                        }
                        return runFiles(obj.full, options, results);
                    });
                } else {
                    // add directories to the results (after all files)
                    if (options.includeDirs) {
                        results.push(...localDirs);
                    }
                }
            });
        });
    }

    return runFiles(rootDir, options, results).then(() => {
        // post process results based on options
        if (options.results === "arrayOfFilePaths") {
            return results.map(item => item.full);
        } else {
            return results;
        }
    });
}

// get flat array of file paths, 
//     recursing into directories, 
//     each directory sorted separately
listFiles(root, {recurse: true, results: "arrayOfFilePaths", sort: true, includeDirs: false}).then(list => {
    for (const f of list) {
        console.log(f);
    }
}).catch(err => {
    console.log(err);
});

你可以将此代码复制到一个文件中并运行它,将 . 作为参数传递以列出脚本的目录或您想要列出的任何子目录名称。
如果你想要更少的选项(如没有递归或者不保留目录顺序),这段代码可以大幅减少,也许可以做得更快(运行一些异步操作并行)。

我建议更新答案,加入require('fs').promises的示例,因为它可能适用于未来的读者。 - Estus Flask
@estus - 好的。 - jfriend00
@BrandonBenefield - 我在我的答案中添加了一个实现。因为我不确定您想要哪种类型的选项,所以我编写了一个通用版本,可以接受多个选项,这样您就可以确定是否要递归进入目录,是否要将目录包含在结果中,是否要对每个目录进行排序,以及是否要仅获取文件路径数组,还是要获取一个对象,该对象提供文件名、根目录和目录标志。 - jfriend00

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接