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

Posted

技术标签:

【中文标题】NodeJS:关于异步“readdir”和“stat”的混淆【英文标题】:NodeJS: Confusion about async "readdir" and "stat" 【发布时间】:2018-12-13 07:15:51 【问题描述】:

在文档中,它显示了 readdir 和 stat 的两个版本。两者都有异步和同步版本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 这两个函数的同步版本。虽然使用 async 方法或 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 ***.com/questions/44019316/… 或许你应该看看这个 @BrandonBenefield 它没有明确说它不返回任何东西,但它返回了它会明确表示的东西(例如“返回一个承诺”)。既然它没有这么说,那么你不能假设它返回一个承诺 嗯,在堆栈溢出时,您不应该将您的答案添加到您的问题中。问题是为了问题。答案是为了答案。两者不应混用。如果您想为自己的问题添加自己的答案,您可以这样做。请从您的问题中删除您的解决方案。它不属于那里。 如果这是服务器端代码,那么在启动时以外的任何地方使用任何同步 I/O 都会对您的服务器可伸缩性造成灾难性影响。它实际上扼杀了可扩展性。 【参考方案1】:

因为 readidir 和 stat 是异步的,我希望它们返回一个 Promise

首先,确保您了解异步函数和async 函数之间的区别。使用 javascript 中的特定关键字声明为 async 的函数,例如:

async function foo() 
    ...

总是返回一个承诺(根据使用 async 关键字声明的函数的定义)。

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

因此,您必须使用回调或“承诺”接口将其转换为返回承诺的内容,以便您可以使用await

有一个 experimental interface in node.js v10 为 fs 模块提供内置承诺。

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

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

在 node.js 的早期版本中,有很多选项可以实现功能。你可以使用util.promisify()逐个函数来完成它:

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

由于我还没有在 node v10 上进行开发,所以我经常使用 Bluebird Promise 库并一次性 Promisify 整个 fs 库:

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 - 会的。 @BrandonBenefield - 我在答案中添加了一个实现。因为我不确定你想要什么类型的选项,所以我写了一个通用版本,它接受许多选项,这样你就可以确定是否要递归到目录中,是否要在结果中包含目录,是否要每个目录排序,如果你只想要一个文件路径数组,或者你想要一个给你文件名、根目录和目录标志的对象。

以上是关于NodeJS:关于异步“readdir”和“stat”的混淆的主要内容,如果未能解决你的问题,请参考以下文章

Node.js fs.readdir 递归目录搜索

Node.js fs.readdir 递归目录搜索

Node.js fs.readdir 递归目录搜索

对fs异步读取文件的理解

如何在遍历一系列目录的循环中使用异步 readdir 函数?

关于nodejs线程的一些困惑