hardfork
hardfork

Reputation: 3251

Asynchronously reading multiple files in for loop

that's my javascript code so far:

files is an array of paths to html files

I want h3s, to be an array of all h3s tags of the html files.

function getH2OfFiles(files) {

  return new Promise((resolve, reject) => {
    let h3s = [];
    for (let i = 0; i < files.length; i++) {
      fs.readFile(path.join(__src, 'documentation', files[i]), 'utf8', (err, data) => {
        if (err) throw err;
        if (data.match(/<h3>(.*)<\/h3>/)) {
          //console.log("->", { file: files[i], h3: data.match(/<h3>(.*)<\/h3>/)[1] })
          h3s.push(data.match(/<h3>(.*)<\/h3>/)[1]);
        }
      })

    }
    resolve(h3s);
  });
}

It does not seem to work in a for loop (because it's asynchronous), but how is it realizable?

Upvotes: 2

Views: 404

Answers (3)

Abhishek Singh
Abhishek Singh

Reputation: 2727

Use the async npm module. They provide synchronous iteration methods as well as different methods for control flow such as series, waterfall, parallel etc.

Upvotes: 0

T.J. Crowder
T.J. Crowder

Reputation: 1074208

You're most of the way there, you just have to keep track of how many callbacks you've gotten and wait to resolve until you've gotten them all. Also, use reject(err) rather than throw err if there's a problem:

function getH2OfFiles(files) {

  return new Promise((resolve, reject) => {
    let h3s = [];
    let complete = 0;
    for (let i = 0; i < files.length; i++) {
      fs.readFile(path.join(__src, 'documentation', files[i]), 'utf8', (err, data) => {
        if (err) reject(err);                       // ***
        const match = data.match(/<h3>(.*)<\/h3>/);
        if (match) {                                // ***
          h3s.push(match[1]);
        }
        if (++complete === files.length) {          // ***
          resolve(h3s);                             // ***
        }                                           // ***
      })
    }
  });
}

(Note that I've also saved the result of the first call to match rather than making the regex run twice.)

But, beware that you can receive those completions out of order, so h3s may be out of order with the files (if it matters).

Or simplify through divide-and-conquer and give yourself a Promise-ified version of readFile and then build the results once all files are read, via Promise.all:

function readFilePromise(path, encoding) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, encoding, (err, data) => {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
}

function getH2OfFiles(files) {
  return Promise.all(files.map(file => readFilePromise(path.join(__src, 'documentation', file), 'utf8')))
         .then(results => {
           const h3s = [];
           results.forEach(result => {
             const match = data.match(/<h3>(.*)<\/h3>/);
             if (match) {
               h3s.push(match[1]);
             }
           });
           return h3s;
         });
}

This also has the advantage that Promise.all ensures you receive the array in the same order as the original array of promises (if it matters).

(Note: There are libs that will Promise-ify NodeJS APIs for you.)

Upvotes: 1

georg
georg

Reputation: 214949

A counterpart for the for loop in the promise world is Promise.all, often combined with .map. In your case, write a function that handles one file, e.g

function getH3OfFile(fileName) {
    return new Promise((resolve, reject) => {
        fs.readFile(path.join('.....', fileName), 'utf8', (err, data) => 
            err
                ? reject(err)
                : resolve(data.match(/<h3>(.*)<\/h3>/))
        );
    });
}

and then apply it to the list of files:

let fileNames = [...]

Promise
   .all(fileNames.map(getH3OfFile))
   .then(h3s => ...) // h3s is an array of matches

(Just in case you're not aware, there's also readFileSync).

Upvotes: 2

Related Questions