Javascript - 所有嵌套的forEach循环完成后回调

5

我相信这是一个相当简单的任务,但目前我无法理解它。我有一组嵌套的forEach循环,需要在所有循环运行完成后有一个回调。

我可以使用async.js库。

以下是我的代码:

const scanFiles = function(accounts, cb) {
  let dirs = ['pending', 'done', 'failed'];
  let jobs = [];

  accounts.forEach(function(account) {
    dirs.forEach(function(dir) {
      fs.readdir(account + '/' + dir, function(err, files) {
         files.forEach(function(file) {
            //do something
            //add file to jobs array
            jobs.push(file);
         });
      });
    });
  });

  //return jobs array once all files have been added
  cb(jobs);
}

考虑使用glob - zzzzBov
scanFiles 返回 Promise 而不是使用回调函数,这样做可以吗? - Diego ZoracKy
6个回答

8

使用forEach的第二个参数,即索引,可以在运行最内层循环时检查是否完成了所有循环。

因此,只需添加几行代码即可实现以下功能:

const scanFiles = function(accounts, cb) {
    let dirs = ['pending', 'done', 'failed'];
    let jobs = [];

    accounts.forEach(function(account, accIndex) {
        dirs.forEach(function(dir, dirIndex) {
            fs.readdir(account + '/' + dir, function(err, files) {
                files.forEach(function(file, fileIndex) {
                    //do something
                    //add file to jobs array
                    jobs.push(file);

                    // Check whether each loop is on its last iteration
                    const filesDone = fileIndex >= files.length - 1;
                    const dirsDone = dirIndex >= dirs.length - 1;
                    const accsDone = accIndex >= accounts.length - 1;

                    // all three need to be true before we can run the callback
                    if (filesDone && dirsDone && accsDone) {
                        cb(jobs);
                    }
                });
            });
        });
    });
}

正如我在编辑拒绝中所说,最好的做法是检查是否大于。以防出现某个地方的偏移错误,这可能会导致无限循环。 - Aron
1
太棒了!因为你使用纯JS完成了任务,我授予你正确答案的奖励。谢谢! - Reza Karami
越界错误?数组是从0开始计数的。 `let dirs = ['pending', 'done', 'failed']; // dirs.length = 3; 最后一个索引为2; 因此,last = 2 === dirs.length - 1; - Luis Estevez
@LuisEstevez,严格的相等性检查在这段代码中可以完美地工作,但经验表明,将故障保护机制构建到您的代码中总是值得的;即使您认为它不是严格必要的。因此使用了>=。 - Aron
只需要一个本来不起眼的错误在错误的地方将fileIndex增加1,你就会突然陷入无限循环。使用>=可以防止一个小错误变成一个大问题。 - Aron
显示剩余3条评论

3

更简单的解决方案

无需循环和推入数组操作

我注意到这里所有的答案都使用了很多复杂的代码。 你可以让它变得更简单:

let fs = require('mz/fs');
let path = require('path');

let d = ['pending', 'done', 'failed'];
let a = ['A', 'B', 'C']; // <-- example accounts

let paths = [].concat.apply([], d.map(d => (a.map(a => path.join(d,a)))));
Promise.all(paths.map(path => fs.readFile(path, 'utf-8'))).then(files => {
  // you have all data here
}).catch(error => {
  // handle errors here
});

解释

如果您使用fs的promise版本-目前可用:

let fs = require('mz/fs');

使用mz模块:

而且很快它将成为Node的本地模块,详情请查看:

这样你就可以像下面的代码一样做事情。使用数据:

// directories:
let d = ['pending', 'done', 'failed'];
// accounts:
let a = ['A', 'B', 'C'];

您可以轻松创建路径数组:

let paths = [].concat.apply([], d.map(d => (a.map(a => path.join(d,a)))));

你可以从中创建一个 Promise 数组:

let promises = paths.map(path => fs.readFile(path, 'utf-8'));

您甚至可以使用Promise.all()来读取所有文件:

let data = Promise.all(promises);

现在你可以把所有东西都用作:
data.then(files => {
  // you have everything ready here
}).catch(error => {
  // some error happened
});

注意:为了让上述代码正常运行,您需要引用两个模块。
let fs = require('mz/fs');
let path = require('path');

我喜欢你添加的解决方案,使用列表推导式肯定更简单。 - Luis Estevez
这很棒。我需要学习一下才能完全理解数组操作。干杯 - Reza Karami

0

你可以使用walk

  walker.on("end", function () {
    console.log("all done");
    cb(jobs);
  });

0

简单计数器

一种简单的方法就是保持一个计数器。

const scanFiles = function(accounts, cb) {
  let dirs = ['pending', 'done', 'failed'];
  let jobs = [];

  // Variables to keep track of
  const lastAccountIndex = accounts.length * dirs.length;
  let indexCounter = 0;

  accounts.forEach(function(account) {
    dirs.forEach(function(dir) {  
      fs.readdir(account + '/' + dir, function(err, files) {
        files.forEach(function(file) {
          //do something
          //add file to jobs array
          jobs.push(file);

          indexCounter++;
        });

        //return jobs array once all files have been added
        if (lastAccountIndex === indexCounter) {
          cb(jobs);
        }
      });
    });
  }); 
}

Promise

另外,fs + Promise 在这里非常有用。

const scanFiles = function(accounts) {
  let dirs = ['pending', 'done', 'failed'];
  let jobs = [];

  const filePromises = []; 
  accounts.forEach(function(account) {
    dirs.forEach(function(dir) {
      filePromises.push(new Promise((resolve, reject) => {
        fs.readdir(account + '/' + dir, function(err, files) {
          files.forEach(function(file) {
            resolve(file);
          });
        });
      }));
    });
  });
  return Promise.all(filePromises);
}

scanFiles(someAccounts)
.then((files) => {
    files.forEach((file) => {
    // At this point, iwll the files will be scanned
    // So, do whatever you want with all the files here.
  });
});

fs-promise

或者直接使用https://www.npmjs.com/package/fs-promise


以这种方式保持计数器确实有效,但仅适用于每个帐户具有相同数量目录的特定情况。 - Aron

0
如果你使用async库https://caolan.github.io/async/docs.html,你的代码将会更快。(forEach是阻塞的[JavaScript, Node.js:Array.forEach是异步的吗?])。
const scanFiles = function (accounts, cb) {
let dirs = ['pending', 'done', 'failed'];
let jobs = [];

async.each(accounts, function (account, accountCallback) {
    async.each(dirs, function (dir, dirCallback) {

        fs.readdir(account + '/' + dir, function (err, files) {
            if(err) console.log(err);

            async.each(files, function (file, fileCallback) {
                //do something
                //add file to jobs array
                jobs.push(file);
                fileCallback();

            }, dirCallback);

        });
    }, accountCallback);
}, function (err) {
    //return jobs array once all files have been added
    if (err) throw err;
    cb(jobs)
});

};


-1
所以问题在于您在fs.readdir完成之前发送了一个空结果,因为nodeJS是异步的。解决方案是将回调函数添加到fs.readdir函数中。
const scanFiles = function (accounts, cb) {
    let dirs = ['pending', 'done', 'failed'];
    let jobs = [];

    accounts.forEach(function (account, i) {
        dirs.forEach(function (dir, j) {
            fs.readdir(account + '/' + dir, function (err, files) {
                files.forEach(function (file, k) {
                    //do something
                    //add file to jobs array
                    jobs.push(file);
                });
                if (i === accounts.length - 1 && j === dirs.length - 1 && k === files.length - 1) {
                    //return jobs array once all files have been added
                    cb(jobs);
                }
            });
        });
    });
}

1
这将运行回调函数多次;每个账户中的每个dir都会运行一次。 - Aron
你是正确的,漏掉了if语句来检查它是否是最后一次循环。 - Luis Estevez

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