【问题标题】:How await a recursive Promise in Javascript如何在 Javascript 中等待递归 Promise
【发布时间】:2019-05-28 23:40:44
【问题描述】:

我在 javascript 中编写了一个递归 Promise,它似乎工作正常,但我想使用 setTimeout() 对其进行测试,以确保在继续执行之前我正在等待正确。这是我的代码的要点:

try{
  await renameFiles(); // <-- await here
  console.log("do other stuff");
}
catch(){
}

const renameFiles = (path) => {
  return new Promise(resolve => {
    console.log("Renaming files...");

    fs.readdirSync(path).forEach(file) => {
      // if file is a directory ...
      let newPath = path.join(path, file);
      resolve( renameFiles(newPath) ); // <- recursion here!
      // else rename file ...
    }
    resolve();
  })

我已经像这样使用 setTimeout() 对其进行了测试:

const renameFiles = () => {
  return new Promise(resolve => {
    setTimeout(() => {
    // all previous code goes here
    },2000)
  }
}

输出是:

"Renaming files..."
"Renaming files..."
// bunch of renaming files...
"do other stuff"
"Renaming files..."
"Renaming files..."

所以看起来它正在等待一段时间,但它会在某个时间点继续执行。

我也怀疑我测试错了。知道问题出在哪里吗?

【问题讨论】:

  • 你多次调用resolve,这是没有意义的。根据the spec,只有第一个分辨率很重要:If the associated promise has already been resolved, either to a value, a rejection, or another promise, this method does nothing.
  • 将 Promise 收集到一个数组中并等待 Promise.all(arrayOfPromises)
  • 我认为@AlexanderAzarov 发现了这个问题。 renameFiles 提前解决,异步函数继续,而递归也继续。可能只需删除 resolve(renameFiles(newPath)) 并仅放置 renameFiels(newPath) 即可修复它。
  • @MichaelSorensen 我已经尝试不使用resolve(renameFiles(newPath)),但它没有解决它

标签: javascript recursion promise async-await


【解决方案1】:

如前所述 - 多次解析调用没有意义。然而,这不是代码中唯一的问题。当对第一个子目录的递归调用开始时,根调用得到解决。此代码将按层次顺序处理目录

rename.js

const fs = require('fs');
const path = require('path');

const inputPath = path.resolve(process.argv[2]);
const newName = 'bar.txt';

async function renameFiles(filePath) {
    for (const file of fs.readdirSync(filePath)) {
        const newPath = path.join(filePath, file);
        const descriptor = fs.lstatSync(newPath);
        if (descriptor.isDirectory()) {
            await renameFiles(newPath)
        } else if (descriptor.isFile()) {
            await renameFile(file);
        }
    }
}

async function renameFile(file) {
    console.log(`Renaming ${file} to ${newName}`)
    return new Promise(resolve => {
       setTimeout(() => {
           console.log(`Renamed ${file} to ${newName}`)
           resolve();
       }, 300)
    });
}

async function main() {
    console.log(`Renaming all files in ${inputPath} to ${newName}`);
    await renameFiles(inputPath);
    console.log('Finished');
}

main();

你可以运行它

node rename.js relativeFolderName

或者如果顺序无关紧要,那么您可以使用@Tiago Coelho 提到的mapPromise.all

const renameFiles = async path => {
    const renamePromises = fs.readdirSync(path).map(file => {
      if (isDirectory(file)) {
          const newPath = path.join(path, file);
          return renameFiles(newPath)
      } else {
          return renamefile(file);
      }  
    });
    await Promise.all(renamePromises);
}

【讨论】:

  • 良好的清洁解决方案。投票赞成。在您的第二个解决方案中,您需要返回承诺,而不是等待,因此它不需要是异步的。此外,您在地图之后还有一些额外的 () 不会真正起作用
  • 对,第二个解决方案有错误,将修复,谢谢。我想要像(async () =&gt; console.log('Async!'))() 这样的东西来匿名异步/等待执行
  • 它仍然对我不起作用。使用@udalmik 给出的解决方案,我的代码看起来像这样return new Promise(resolve=&gt;{ setTimeout(async()=&gt;{ // CODE HERE },2000) ,最终永远无法解决。在 for 循环之后添加 resolve() 也最终无法正确等待。 })
  • 抱歉,没有找到您的问题,或者您想用 setTimeout 解决什么问题。通过测试运行更新了答案。
  • 在这个答案中付出了多么令人印象深刻的努力。感谢您的耐心。
【解决方案2】:

要完成这项工作,您需要等待目录中的所有文件解析。所以你需要做一个Promise.all 并使用map 而不是forEach

类似这样的:

try{
  await renameFiles(); // <-- await here
  console.log("do other stuff");
}
catch(){
}

const renameFiles = (path) => {
  return new Promise(resolve => {
    console.log("Renaming files...");

    const allFilesRenamePromises = fs.readdirSync(path).map(file => {
      if(file.isDirectory()) {
        let newPath = path.join(path, file);
        return renameFiles(newPath); // <- recursion here!
      } else {
        // rename file ...
      }
    }
    resolve(Promise.all(allFilesRenamePromises));
  })

【讨论】:

  • 您应该使用问题中带有await 的原始代码,而不是Pavithra 答案中带有.then() 的原始代码。
【解决方案3】:

与其编写一个大而复杂的函数,我会建议一种更分解的方法。

首先我们从files 开始,它递归地列出指定path 处的所有文件-

const { readdir, stat } =
  require ("fs") .promises

const { join } =
  require ("path")

const files = async (path = ".") =>
  (await stat (path)) .isDirectory ()
    ? Promise
        .all
          ( (await readdir (path))
              .map (f => files (join (path, f)))
          )
        .then
          ( results =>
             [] .concat (...results)
          )
    : [ path ]

我们现在有办法列出所有文件,但我们只想重命名其中的一些。我们将编写一个通用的search 函数来查找与查询匹配的所有文件-

const { basename } =
  require ("path")

const search = async (query, path = ".") =>
  (await files (path))
    .filter (x => basename (x) === query)

现在我们可以将您的 renameFiles 函数编写为 search 的特化 -

const { rename } =
  require ("fs") .promises

const { dirname } =
  require ("path")

const renameFiles = async (from = "", to = "", path = ".") =>
  Promise
    .all
      ( (await search (from, path))
          .map
            ( f =>
                rename
                  ( f
                  , join (dirname (f), to)
                  )
             )
       )

要使用它,我们只需调用 renameFiles 及其预期参数 -

renameFiles ("foo", "bar", "path/to/someFolder")
  .then
    ( res => console .log ("%d files renamed", res.length)
    , console.error
    )

// 6 files renamed

查看我们上面的程序,我们发现使用Promise.allawaitmap 出现了一些模式。事实上,这些模式可以被提取出来,我们的程序可以进一步简化。这是 filesrenameFiles 修改为使用通用 Parallel 模块 -

const files = async (path = ".") =>
  (await stat (path)) .isDirectory ()
    ? Parallel (readdir (path))
        .flatMap (f => files (join (path, f)))
    : [ path ]

const renameFiles = (from = "", to = "", path = "") =>
  Parallel (search (from, path))
    .map
      ( f =>
          rename
            ( f
            , join (dirname (f), to)
            )
      )

Parallel 模块最初派生于this related Q&A。如需更多见解和解释,请点击链接。

【讨论】:

    【解决方案4】:

    在我的first answer 中,我向您展示了如何主要使用功能技术来解决您的问题。在这个答案中,我们将看到诸如异步迭代等现代 JavaScript 特性使这种事情变得更加容易 -

    const files = async function* (path = ".")
    { if ((await stat (path)) .isDirectory ())
        for (const f of await readdir (path))
          yield* files (join (path, f))
      else
         yield path
    }
    
    const search = async function* (query, path = ".")
    { for await (const f of files (path))
        if (query === basename (f))
          yield f
    }
    
    const renameFiles = async (from = "", to = "", path = ".") =>
    { for await (const f of search (from, path))
        await rename
          ( f
          , join (dirname (f), to)
          )
    }
    
    renameFiles ("foo", "bar", "path/to/someFolder")
      .then (_ => console .log ("done"), console.error)
    

    【讨论】:

      【解决方案5】:

      为了完整起见,我将根据 @udalmik 的建议发布整个解决方案。唯一的区别是我将async function renameFile(file) 包装在Promise 中。

      const fs = require('fs');
      const path = require('path');
      
      const inputPath = path.resolve(process.argv[2]);
      const newName = 'bar.txt';
      
      async function renameFiles(filePath) {
          for (const file of fs.readdirSync(filePath)) {
              const newPath = path.join(filePath, file);
              const descriptor = fs.lstatSync(newPath);
              if (descriptor.isDirectory()) {
                  await renameFiles(newPath)
              } else if (descriptor.isFile()) {
                  await renameFile(file);
              }
          }
      }
      
      async function renameFile(file) {
        return new Promise(resolve => {
          console.log(`Renaming ${file} to ${newName}`);
          resolve();
        })
      }
      
      async function main() {
          console.log(`Renaming all files in ${inputPath} to ${newName}`);
          await renameFiles(inputPath);
          console.log('Finished');
      }
      
      main();
      

      使用 Promise 的原因是我想在继续执行之前等待所有文件被重命名(即console.log('Finished');)。

      我已经测试过使用 setTimeout

      return new Promise(resolve => {
          setTimeout(()=>{
            console.log(`Renaming ${file} to ${newName}`);
          },1000)
          resolve(); // edited missing part
        })
      

      解决方案与我原来的问题不同,但我想它对我有用。

      【讨论】:

      • 实际上,在readFile 中用假Promise 包装结果并没有改变任何东西。请查看this tutorial中的异步函数部分
      • @udalmik 感谢您的链接。我想我了解如何使用等待,但也许在使用 setTimeout 时我会错过一些东西?有更好/不同的测试方法吗?如果您不将函数包装在 Promise 中,则输出将不是“同步的”(使用 setTimeout() 时)
      • 如果你想模拟重命名文件的一些延迟 - 那么 setTimeout 是一种方法,你只是错过了解决 fn 的电话,我已经在我的答案中添加了延迟。我想指出,用 promise(你的第一个代码 sn-p)和立即解析调用来包装异步操作的结果是没有意义的,JS 引擎会为你做这件事。
      • @udalmik 是的,我知道没有必要将结果包装在 Promise 中。我这样做是因为在使用setTimeout 时这是必要的。这就是让我感到困惑的部分:为什么我需要将 setTimeout 包装在 Promise 中以使其工作?
      【解决方案6】:

      尝试像这样更改等待代码。这可能会对您有所帮助。

      try{
        const renameFilesPromise = renameFiles();
        renameFilesPromise.then({      <-- then is a callback when promise is resolved
          console.log("do other stuff");
        })
      }
      catch(){
      }
      
      const renameFiles = (path) => {
        return new Promise(resolve => {
          console.log("Renaming files...");
      
          fs.readdirSync(path).forEach(file) => {
            // if file is a directory ...
            let newPath = path.join(path, file);
            resolve( renameFiles(newPath) ); // <- recursion here!
            // else rename file ...
          }
          resolve();
        })
      

      【讨论】:

      • 我还没有测试它,它可能工作,但我试图避免回调地狱。在那次电话之后,我有一堆等待。
      • 好的,你可以使用 await 本身。尝试增加 setTimeOut。原因 setTimeOut 会在 2000 毫秒内自动解决。
      • 这也不起作用,因为当您调用第一个 resolve(renameFiles(newPath)) 时,您实际上会忽略循环中的剩余文件
      猜你喜欢
      • 2018-04-17
      • 2022-01-07
      • 2016-09-18
      • 2021-06-30
      • 1970-01-01
      • 1970-01-01
      • 2020-01-15
      • 2017-07-28
      • 2019-02-20
      相关资源
      最近更新 更多