【问题标题】:How to handle multiple promises at once如何一次处理多个 Promise
【发布时间】:2020-04-22 23:23:06
【问题描述】:

我正在创建一个程序...

           1. 检测任何给定系统上的所有驱动器。

           2. 扫描这些驱动器以查找特定文件类型的文件。例如,
              它可能会在所有驱动器中搜索任何 jpegpngsvg 文件。

           3. 然后将结果以以下所需格式存储在 JSON 文件中。

{
  "C:": {
    "jpeg": [
      ...
      {
        "path": "C:\\Users\\John\\Pictures\\example.jpeg",
        "name": "example",
        "type": "jpeg",
        "size": 86016
      },
      ...
    ],
    "png": [],
    "svg": []
  },
  ...
}

代码...

async function scan(path, exts) {
  try {
    const stats = await fsp.stat(path)
    if (stats.isDirectory()) {
      const
        childPaths = await fsp.readdir(path),
        promises = childPaths.map( 
          childPath => scan(join(path, childPath), exts)
        ),
        results = await Promise.all(promises)


      // Likely needs to change.
      return [].concat(...results)


    } else if (stats.isFile()) {
      const fileExt = extname(path).replace('.', '')
      if (exts.includes(fileExt)){


        // Likely needs to change.
        return {
          "path": path,
          "name": basename(path, fileExt).slice(0, -1),
          "type": fileExt,
          "size": stats.size
        }


      }
    }
    return []
  }
  catch (error) {
    return []
  }
}


const results = await Promise.all(
  config.drives.map(drive => scan(drive, exts))
)

console.log(results) // [ Array(140), Array(0), ... ]

// And I would like to do something like the following...

for (const drive of results) {
  const
    root = parse(path).root,
    fileExt = extname(path).replace('.', '')
  data[root][fileExt] = []
}

await fsp.writeFile('./data.json', JSON.stringify(config, null, 2))

全局results 当然被划分为与每个驱动器对应的单独数组。但目前它将所有对象组合成一个巨大的数组,尽管它们有相应的文件类型。我目前也无法知道每个驱动器属于哪个数组,尤其是如果驱动器的数组不包含任何我可以解析以检索根目录的项目。

我显然可以map 或再次循环通过全局results,然后将所有内容整理出来,如下图所示,但是让scan() 从一开始就处理所有事情会更干净。

// Initiate scan sequence.
async function initiateScan(exts) {
  let
    [config, data] = await Promise.all([
      readJson('./config.json'),
      readJson('./data.json')
    ]),
    results = await Promise.all(
      // config.drives.map(drive => scan(drive, exts))
      ['K:', 'D:'].map(drive => scan(drive, exts))
    )
  for (const drive of results) {
    let root = false
    for (const [i, file] of drive.entries()) {
      if (!root) root = parse(file.path).root.slice(0,-1)
      if (!data[root][file.type] || !i) data[root][file.type] = []
      data[root][file.type].push(file)
    }
  }
  await fsp.writeFile('./data.json', JSON.stringify(config, null, 2))
}

由于我对异步和一般对象缺乏经验,我不太确定如何最好地处理map( ... )/scan 中的数据。我什至不确定如何最好地构造scan() 的输出,以便全局results 的结构易于操作。

任何帮助将不胜感激。

【问题讨论】:

  • “然后将结果存储在具有以下格式的 JSON 文件中......”。您的意思是结果应该以这种方式存储吗?
  • @Roamer-1888 是的,这就是我的意思。这就是我想存储它们的方式

标签: javascript node.js asynchronous promise fs


【解决方案1】:

在异步派生的结果到达时改变外部对象并不是特别干净,但是它可以相当简单和安全地完成,如下所示:

(async function(exts, results) { // async IIFE wrapper
    async function scan(path) { // lightly modified version of scan() from the question.
        try {
            const stats = await fsp.stat(path);
            if (stats.isDirectory()) {
                const childPaths = await fsp.readdir(path);
                const promises = childPaths.map(childPath => scan(join(path, childPath)));
                return Promise.all(promises);
            } else if (stats.isFile()) {
                const fileExt = extname(path).replace('.', '');
                if (results[path] && results[path][fileExt]) {
                    results[path][fileExt].push({
                        'path': path,
                        'name': basename(path, fileExt).slice(0, -1),
                        'type': fileExt,
                        'size': stats.size
                    });
                }
            }
        }
        catch (error) {
            console.log(error);
            // swallow error by not rethrowing
        }
    }
    await Promise.all(config.drives.map(path => {
        // Synchronously seed the results object with the required data structure
        results[path] = {};
        for (fileExt of exts) {
            results[path][fileExt] = []; // array will populated with data, or remain empty if no qualifying data is found.
        }
        // Asynchronously populate the results[path] object, and return Promise to the .map() callback
        return scan(path);
    }));
    console.log(results);
    // Here: whatever else you want to do with the results.
})(exts, {}); // pass `exts` and an empty results object to the IIFE function.

结果对象使用空数据结构同步播种,然后异步填充。

所有内容都包含在异步立即调用函数表达式 (IIFE) 中,因此:

  • 避免使用全局命名空间(如果尚未避免)
  • 确保await 的可用性(如果尚不可用)
  • results 对象创建一个安全的闭包。

【讨论】:

  • 我通常会否决一个建议在map 这样的操作中改变外部对象的答案,但这是解决Promise.all 限制的巧妙方法,以便生成所需的导致单次通过。不错!
  • 对于函数的成功,IIFE 是绝对必要的,还是我可以简单地摆脱 IIFE,然后直接调用函数?我尝试通过删除 IIFY 来稍微修改您的代码以匹配我的整个脚本,给它一个名称(startScan),直接调用它(const results = await startScan(exts, {})),然后将config.drives 更改为['D:', 'K:'] 以节省一些时间,并且,尽管它返回了一个具有正确结构的对象,但由于某种原因,嵌套数组在扫描后是空的??
  • 或者实际上我刚刚意识到这不是我的console.log(results) 来自一个单独的记录功能,而是底部的console.log(results)
  • 不摆脱 IIFE 的好理由。它封装了整个过程,并有助于避免与外部范围内的任何其他内容发生意外交互。
  • 是的,我包括 IIFE 主要是因为没有证据表明函数包装器的问题。确保你的函数是asyncresults是在内部声明的,所有的异步都在等待,results被返回。
【解决方案2】:

这还需要一些工作,它正在第二次迭代生成的文件集合。

// This should get you an object with one property per drive
const results = Object.fromEntries(
  (await Promise.all(
      config.drives.map(async drive => [drive, await scan(drive, exts)])
    )
  )
  .map(
    ([drive, files]) => [
      drive,
      // we reduce each drive's file array to an object with
      // one property per file extension
      files.reduce(
        (acc, file) => {
          acc[file.type].push(file)
          return acc
        },
        Object.fromEntries(exts.map(ext => [ext, []]))
      )
    ]
  )
)

nodejs 从 12.0.0 版本开始支持 Object.fromEntries,所以如果你能保证你的应用程序将始终在该版本或更高版本中运行,Object.fromEntries 在这里应该没问题。

【讨论】:

  • 这很接近我想要的。男人很困难,因为有多层
  • 是的,我的初始代码的示例输出是[ Array(167), Array(0) ... ],每个数组对应一个驱动器。
【解决方案3】:

您可以使用glob npm library 获取所有文件名,然后将该数组转换为您的对象,如下所示:

import {basename, extname} from 'path';
import {stat} from 'fs/promises'; // Or whichever library you use to promisify fs
import * as glob from "glob";

function searchForFiles() {
    return new Promise((resolve, reject) => glob(
        "/**/*.{jpeg,jpg,png,svg}", // The files to search for and where
        { silent: true, strict: false}, // No error when eg. something cannot be accessed 
        (err, files) => err ? reject() : resolve(files)
    ));
}

async function getFileObject() {
    const fileNames = await searchForFiles(); // An array containing all file names (eg. ['D:\\my\path\to\file.jpeg', 'C:\\otherfile.svg'])

    // An array containing all objects describing your file
    const fileObjects = await Promise.all(fileNames.map(async filename => ({ 
        path: filename,
        name: basename(path, fileExt).slice(0, -1),
        type: extname(path).replace('.', ''),
        size: stat(path).size,
        drive: `${filename.split(':\\')[0]}:`
    })));

    // Create your actual object
    return fileObjects.reduce((result, {path, name, type, size, drive}) => {
        if (!result[drive]) { // create eg. { C: {} } if it does not already exist
            result.drive = {};
        }
        if (!result[drive][type]) { // create eg. {C: { jpeg: [] }} if it does not already exist
            result[drive][type] = [];
        }
        // Push the object to the correct array
        result[drive][type].push({path, name, type, size});
        return result;
    }, {});
}

【讨论】:

  • 我想你的答案会奏效,但它似乎效率极低。为什么要循环通过“相同”的数据/输出 3 次?处理scan() 中的所有排序等不是更好吗,这样输出就已经是所需的格式,以避免再循环 2 次??
  • 您希望您的阵列有多大?考虑到到目前为止最慢的部分是对底层文件系统的访问,循环本身的开销可以忽略不计。即使您的数组有一百万个条目,循环也将是几分之一秒。
  • 我不是 100% 确定,但每个驱动器可能或多或少有数万个文件。我在自己的 PC 上安装了 9 个驱动器。我知道仅仅通过数组循环不会占用很多时间,但如果我也解析其中的每个对象,等等
  • 在这种情况下,循环完全不用担心。我只是仔细检查了一遍:即使您在浏览器中运行这些循环(显然没有文件系统访问权限)有 100 万个条目,每个循环大约需要 200 毫秒,包括创建对象。
  • 另外,您不会遍历相同的数据 3 次。 glob 库所做的第一个“循环”正在遍历整个文件系统。这很容易达到几十亿个条目。第二个和第三个循环仅针对其中的一个子集运行 - 所有符合您的搜索条件的文件。相比之下,第二个和第三个循环就显得苍白了很多。
【解决方案4】:

该函数必须递归遍历文件系统,寻找符合您条件的文件。由于结果不需要保留任何层次结构,因此可以简化递归,因此我们可以只携带一个平面数组 (files) 作为参数。

let exts = [...]

async function scan(path, files) {
  const stats = await fsp.stat(path)
  if (stats.isDirectory()) {
    childPaths = await fsp.readdir(path)
    let promises = childPaths.map(childPath => {
      return scan(join(path, childPath), files)
    })
    return Promise.all(promises)
  } else if (stats.isFile()) {
    const fileExt = extname(path).replace('.', '')
    if (exts.includes(fileExt)) {
      files.push({
        path: path,
        name: basename(path, fileExt).slice(0, -1),
        type: fileExt,
        size: stats.size
      })
    }
  }
}

let files = []
await scan('/', files)
console.log(files)

【讨论】:

  • 问题是我使用exec('wmic logicaldisk get name') 检测和扫描给定系统上的所有驱动器。我希望能够将结果存储在我的问题开头概述的结构中的 json 文件中,如果scan() 只是返回代表每个文件的平面对象数组,那么我必须循环整个数组并再次一一解析对象,这似乎效率低下。那有意义吗?也许我最初的问题措辞不够好
  • 我希望扫描所有驱动器,并在此过程中对它们进行排序,以避免多次循环“相同”数据/输出
猜你喜欢
  • 2012-04-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-04-24
  • 1970-01-01
  • 2019-03-30
相关资源
最近更新 更多