Javascript - Callback after all nested forEach loops are completed

孤街浪徒 提交于 2020-08-24 13:21:58

问题


I'm sure this is a fairly simple task, but I'm not able to wrap my head around it at this time. I've got a nested set of forEach loops, and I need to have a callback for when all the loops are done running.

I'm open to using async.js

This is what I'm working with:

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);
}

回答1:


Using forEach's 2nd parameter, the index, you can carry out a check whether all loops are done each time you run the innermost loop.

Thus with only a few lines added to your code you get this:

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);
                    }
                });
            });
        });
    });
}



回答2:


Simpler solution

No need for loops and pushing to arrays

I noticed that all of the answers here use a lot of complicated code. You can make it much simpler:

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
});

Explanation

If you use the promise version of fs - currently you can use:

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

with the mz module:

  • https://www.npmjs.com/package/mz

and soon it will be native in Node, see:

  • https://github.com/nodejs/node/pull/5020

then you will be able to do things like the code below. Using the data:

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

You can easily create an array of paths:

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

From which you can create an array of promises:

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

You can even use Promise.all() to have all of your files read:

let data = Promise.all(promises);

Now you can use everything as:

data.then(files => {
  // you have everything ready here
}).catch(error => {
  // some error happened
});

Note: you need to require two modules for the above code to work:

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



回答3:


You can use walk

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



回答4:


Simple counter

One simple way could be just to keep a counter.

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

Alternatively, fs + promise could be very useful here.

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

Or just use https://www.npmjs.com/package/fs-promise




回答5:


If you use asyc library https://caolan.github.io/async/docs.html, your code will be much faster. (forEach is blocking [JavaScript, Node.js: is Array.forEach asynchronous?).

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)
});

};




回答6:


So the problem is that you were sending an empty result before fs.readdir was done because nodeJS is async. So the solution is to add the callback inside the fs.readdir function.

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);
                }
            });
        });
    });
}


来源:https://stackoverflow.com/questions/43052015/javascript-callback-after-all-nested-foreach-loops-are-completed

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!