Buzinas
Buzinas

Reputation: 11725

Promises inside a recursive function

I have the following code:

function load(lab, el) {
  return Promise.all([Util.loadHtml(lab), Util.loadScript(lab)])
    .then(function(responses) {
      parse(responses[0], el, responses[1]);
    });
}

function parse(html, parent, context) {
  var children = [].slice.call(html.childNodes).filter(function (item) { return item.nodeType === 1 || item.nodeType === 3; });

  for (var i = 0; i < children.length; i++) {
    var child = children[i];

    if (child.tagName.indexOf('-') >= 0) {
      load(child.tagName.toLowerCase(), parent);
    }
    else {
      var parsedNode = parseNode(child, context);
      parent.appendChild(parsedNode);
      if (child.hasChildNodes())
        parse(child, parsedNode, context);
    }
  }
}

Basically, that's what it should do:

  1. In my app.js I call the load function, that will import two files, one html and one js, and when the promise of those requests is fullfilled, it calls a function named parse which will loop into the HTML and will parse some strings with the class declared in the JS file.
  2. Inside the loop, it's possible to find some custom tag, for example <my-element>, and then, it will try to load my-element.html and my-element.js, and it will loop inside that HTML too.
  3. As you can see in the code, I pass the parent and the context, so after finishing all the loopings, the 'big' parent should have all the other components inside it.

The problem

Since the load function returns a promise, and I'm calling it synchronously, it immediately returns, and for that reason, the children is not placed inside the correct parents.

If I was doing that in C#, for example, or with the ES7 async and await keywords, that would be pretty easy. But I have no idea how can I call that load function asynchronously. Any guesses?

Upvotes: 2

Views: 1751

Answers (4)

Buzinas
Buzinas

Reputation: 11725

In the end, it was simpler than I thought it would be:

function load(lab, el) {
  return Promise.all([Util.loadHtml(lab), Util.loadScript(lab)])
    .then(function(responses) {
      return parse(responses[0], el, responses[1]); // return here
    });
}

function parse(html, parent, context) {
  var children = [].slice.call(html.childNodes).filter(function (item) { return item.nodeType === 1 || item.nodeType === 3; });

  for (var i = 0; i < children.length; i++) {
    var child = children[i];

    if (child.tagName.indexOf('-') >= 0) {
      return load(child.tagName.toLowerCase(), parent); // return here
    }
    else {
      var parsedNode = parseNode(child, context);
      parent.appendChild(parsedNode);
      if (child.hasChildNodes())
        parse(child, parsedNode, context);
    }
  }
}

Since my parsing must be synchronous (because of the order etc), and the only thing I needed was waiting for the load function to finish before going back to parse, the only things I changed was to instead of directly calling the parse function inside the load one and vice-versa, I'm now using the return, since then it will wait for the execution, before going back to the caller.


Other thing that worked and it was even better for my use case: I ended up creating a clone of my custom element, and appending it to the parent, calling the loading function passing it. Doing that, I could load all its children async and without the problem of not being attached to the DOM.

Faster execution, and better readability!

Upvotes: 0

fuyushimoya
fuyushimoya

Reputation: 9813

You can use .reduce to achieve that:

function load(lab, el) {
  return Promise.all([Util.loadHtml(lab), Util.loadScript(lab)])
    .then(function(responses) {
      return parse(responses[0], el, responses[1]);
    });
}

function parse(html, parent, context) {
  var children = [].slice.call(html.childNodes).filter(function (item) { return item.nodeType === 1 || item.nodeType === 3; });

  // What this return is a promise chained through all of its children
  // So until all children get resolved, this won't get resolved.
  return children.reduce(function(promise, child, idx) {
    var childPromise = null;
    if (child.tagName.indexOf('-') >= 0) {
      // Start to load the contents, until childPromise is resolved, the final
      // can't be resolved.
      childPromise = load(child.tagName.toLowerCase(), parent);
    } else {
      var parsedNode = parseNode(child, context);
      parent.appendChild(parsedNode);
      // If it has child, also make it return a promise which will be resolved
      // when child's all children parsed.
      if (child.hasChildNodes()) {
        childPromise = parse(child, parsedNode, context);
      }
    }

    // It's either null, which means it'll be resolved immediately,
    // or a promise, which will wait until its childs are processed.
    return promise.then(function() {
      return childPromise;
    });
  }, Promise.resolve());
}

Then, when iteratin through the children, it'll keep chaining the promise, each child can either load or parse independently, and until all children is resolved, the promise return from parse get resolved. So you can now use it like:

parse(THE PARAM OF ROOT).then(function() {
    // All the big parents's children are now parsed.
    console.log('All done');
});

EDITED: as Bergi suggests, Promise.all is better then .reduce, as it'll reject immediately when any of the children(grandchildren) fails. And as The more acceptable answer is posted, I'll just give a link to it, instead of add the .all version.

And JavaScript Promises#chaining may help you too.

Upvotes: 1

Bergi
Bergi

Reputation: 664195

If a function is asynchronous, it should return a promise. Always. Even (or: especially) in then callbacks.

If you produce multiple promises in that loop, you can await them via Promise.all:

function load(lab, el) {
  return Promise.all([Util.loadHtml(lab), Util.loadScript(lab)])
    .then(function(responses) {
      return parse(responses[0], el, responses[1]);
//    ^^^^^^
    });
}

function parse(html, parent, context) {
  var children = [].slice.call(html.childNodes).filter(function (item) { return item.nodeType === 1 || item.nodeType === 3; });

  return Promise.all(children.map(function(child, i) {
//^^^^^^^^^^^^^^^^^^

    if (child.tagName.indexOf('-') >= 0) {
      return load(child.tagName.toLowerCase(), parent);
//    ^^^^^^
    } else {
      var parsedNode = parseNode(child, context);
      parent.appendChild(parsedNode);
      if (child.hasChildNodes())
        return parse(child, parsedNode, context);
//      ^^^^^^
    }
  }));
}

If I was doing that in C#, for example, or with the ES7 async and await keywords, that would be pretty easy. But I have no idea how can I call that load function asynchronously

Yeah, you really should consider using those. Or you can emulate them with ES6 generator functions and a runner (as provided by many popular promise libraries). But you're using a transpiler anyway, right?

Writing load would be quite easy with them:

async function load(lab, el) {
  var responses = await Promise.all([Util.loadHtml(lab), Util.loadScript(lab)]);
  return parse(responses[0], el, responses[1]);
}

Upvotes: 3

MinusFour
MinusFour

Reputation: 14423

I think this is where you got it wrong:

if (child.tagName.indexOf('-') >= 0) {
      load(child.tagName.toLowerCase(), parent);
    }

You are passing the parent of the child object to be the parent of the grandchild object. You probably need to pass child as parent of the grandchild.

Upvotes: 0

Related Questions