user3750761 user3750761 - 5 months ago 21
jQuery Question

Chaining asynchronous function calls in JS?

So I'm fairly new to JavaScript and aware of asynchronous functions calls. I have done quite a bit of research and found that if you want to run asynchronous calls in succession of one another, you can use callback functions and promises. Now I have come to understand how both of these implementations are useful if you are running just a few asynchronous functions. I'm trying to tackle a completely different animal; at least to my knowledge. I'm currently building a site that needs to appear as if it's writing text to itself. Just to clue everyone here in to my JS code, here is the function that writes to the webpage (I'm fairly new so if you think you have a better solution, an example with a small description would be appreciated):



function write(pageText, elementId, delay) {
var element = document.getElementById(elementId);
var charCount = 0;

setInterval(function() {
if (charCount > pageText.length) {
return;
} else {
element.innerHTML = pageText.substr(0, charCount++);
}
}, delay);
}

write("This is an example", 'someRandomDiv', 100);

<div id="someRandomDiv">

</div>





With this I'm trying to write a line of text to a webpage one line after another. Essentially I'm use to writing code like such in Java and C#:

function writePassage()
{
var passage=["message one", "message two", "... message n"];
for(var i = 0; i<passage.length; i++)
{
write(passage[i], 'someRandomDiv', 100);
}
}


Obviously since this won't work because the for loop in wirtePassage() will finish executing before just one or two of the asynchronous function calls end. I'm asking if there is a sane solution to this error where I have n asynchronous calls, and I need to have one perform before the next one is triggered. It's worth mentioning that I don't want to just run this loop above and add another variable that just keeps track of how long I should delay each passage that will be written. I would prefer if there was a programmatic way that forces the execution of the function before the next one is called. Thanks for reading this monster question!

Answer

There are a few things you'll need to do to get this working.

First, your write function will need an asynchronous interface. As you mentioned it could either take a callback or return a promise. Taking a callback would look something like:

function write(pageText, elementId, delay, callback)
{
  var element = document.getElementById(elementId);
  var charCount=0;

  var interval = setInterval(function(){
    if(charCount>pageText.length)
    {
      clearInterval(interval);
      callback();
    }
    else
    {
      element.innerHTML = pageText.substr(0,charCount++);
    }
  }, delay);
}

That calls callback when the full pageText has been written into element. Note that it also clears your interval timer when it's done, which avoids an event loop leak.

Then, you'll need to chain your asynchronous calls using this callback. You can do this quite cleanly with a library such as async:

function writePassage()
{
  var passage=["message one", "message two", "... message n"];
  async.series(passage.map(function(text){
    return function(done){
      write(text, 'someRandomDiv', 100, done);
    };
  }));
}

But it's also not so much trouble to do by hand:

function writePassage()
{
  var passage=["message one", "message two", "... message n"];

  var writeOne = function() {

    if (!passage.length) return;

    var text = passage.shift();

    write(text, 'someRandomDiv', 100, writeOne);
  }

  // Kick off the chain.
  writeOne();
}

That's just asynchronous recursion. Welcome to JavaScript. :)

A promise-based solution can also be pretty clean. First you need to return a promise from write:

function write(pageText, elementId, delay)
{
  return new Promise(resolve) {
    var element = document.getElementById(elementId);
    var charCount=0;

    var interval = setInterval(function(){
      if(charCount>pageText.length)
      {
        clearInterval(interval);
        resolve();
      }
      else
      {
        element.innerHTML = pageText.substr(0,charCount++);
      }
    }, delay);
  }
}

Then you can create a chain of promises via reduction:

function writePassage()
{
  var passage=["message one", "message two", "... message n"];

  passage.reduce(function(chain, text) {
    return chain.then(function(){
      return write(text, 'someRandomDiv', 100, writeOne);
    });
  }, Promise.resolve());
}