Asynchronous Code In JavaScript
Written by Ian Elliot   
Monday, 15 May 2017
Article Index
Asynchronous Code In JavaScript
Asynchronous Flow Of Control and Closure
Custom Asynchronous Functions
Summary

The best way to see how all this works is to look at a general asynchronous call. For example, the Ajax get example we looked at earlier. If you want to load two files then the simplest thing to do is:

$.get("test.txt",function(data){ console.log(data);});
$.get("test_1.txt",function(data){ console.log(data);});

However, you cannot know which file will load first. If you want to always load test and then load test_1 you would have to write something like:

$.get("test.txt", function (data) {
  console.log(data);
  $.get("test_1.txt", function (data) {
    console.log(data);
  });
});

You can see that the second get is only performed when the first get's callback is activated. It isn't difficult to follow this with just two files, but it gets increasingly difficult with more files. This is the origin of "callback hell" as the indentation needed to keep track of callbacks within callbacks increases and increases. 

One solution is to use the function queue. To do this we first need to wrap the functions we want to call and add a call to next at the point we want the next function to start work:

var getQ1 = function (next) {
  $.get("test.txt", function (data) {
          console.log(data);
          next();
      });
};


We also need to wrap the second asynchronous function in the same way:

var getQ2 = function (next) { 
  $.get("test_1.txt", function (data) {
          console.log(data);
          next();
      });
};

Now all we have to do is add them to a queue and start things running:

var q = $({}).queue("myQ",getQ1);
q.queue("myQ",getQ2);
q.dequeue("myQ");

Now the second file will not start loading until the first has been loaded and processed. 

The same sort of approach solves most of the problems of running asynchronous functions sequentially. Notice that the call to next() doesn't have to occur at the very end of the asynchronous function. It can be placed anywhere it is allowable to start the next function in the queue so there can be an overlap.

The big problem is handling any errors that might occur. The most direct approach is to have each of the functions check for an error and then do something appropriate like trying the action again or simply clearing the queue:

if(error) q.clearQueue("myQ");

Notice that in this simple form the functions in the queue have to have access to the object on which the queue is defined and the name of the queue. In this case q is a global variable and the queue name was set earlier.

If you want to restart the entire queue a given number of times then you need to make sure that there is a copy that can be started when an error occurs. 

Queue Functions With Parameters

A particular problem with the function queue is that it cannot be used with functions that require parameters. Some go to the trouble of implementing their own function queue complete with parameters or augmenting the function queue with a separate parameter queue. This isn't necessary because we can use closure to replace parameter passing. This is a general pattern that can be used whenever you need to pass parameters to a function, but cannot use parameters. 

For example, suppose we want to pass the file name to the Ajax file download function. This can be done by creating a function that creates the function we want to add to the function queue - that is we need to use an object factory. The reason is simply that any variables that are in scope when the object factory creates the function will be accessible from that function:

var GETQ = function (file) {
 var f = function (next) {
 $.get(file, function (data) {
        console.log(data);
        next();
     });
 };
 return f;
};

Now GETQ is not the function we are placing in the queue, instead it returns the function that we are placing in the queue. Also, notice that the parameters of GETQ are in scope when the function f is created, and so they can all be used by it courtesy of closure.

Now to add the function to the queue we have to call GETQ with the parameters we want to use:

var q = $({}).queue("myQ",GETQ("test.txt"));
q.queue("myQ",GETQ("test_1.txt"));
q.dequeue("myQ");

Notice that we actually have to call the function so that it creates the function that is placed in the queue.

This is a general pattern that you can use to pass parameters and additional parameters to any function. Simply write an object factory that accepts the parameters and creates the function. The parameters are made available to the created function by the operation of closure.

Custom Asynchronous Functions

Asynchronous functions in JavaScript mainly relate to file transfer and animation. However, you can implement your own asynchronous functions to avoid blocking the UI thread. Key to doing this are the setInterval and setTimeOut functions. These will place a message in the event queue that calls a function after a set time. The setInterval function arranges to call the function repeatedly at the set interval.

You may already be familiar with these functions as ways of calling functions after a delay or regularly, but they can also be used to create a custom asynchronous function. All you have to do is call setTimeOut with a delay of zero:

setTimeout(function(){do something},0);

This  effectively puts the function on the event queue and then returns immediatetly. Of course, the function will not be processed by the event queue until the UI thread is released.

A simple example should make this clear:

console.log("before");
setTimeout(function () {
                        console.log("MyAsyncFunction");                          }, 0);
console.log("after");

The sequence of events is that first "before" is sent to the log, then the function is added to the event queue with a timeout of 0, but the function cannot be called until UI thread is freed. The setTimeout returns at once and "after" is sent to the log and the UI thread is freed, assuming this is the last JavaScript instruction. Only then does the function get to run and send "MyAsyncFunction" to the log.

You can see that the order of execution is not what you might expect and this is asynchronous behavior. Notice also that the event queue is processed in whatever order the events occurred in and if there is an event waiting to be processed it could be done before your custom function is called. You can make use of this to break up a long running function so as to keep the UI responsive. 

The general idea is very simple, but the details vary according to the algorithm. As a simple example, suppose you want to compute pi to a few digits using the series:

pi=4*(1-1/3+1/5-1/7 ... and so on)

This is very easy to implement but to get pi to a reasonable number of digits you have to compute a lot of terms. The simple-minded synchronous approach is to write something like:

$("#go").click(computePi);

function computePi() {
 var pi = 0;
 var k;
 for (k = 1; k <= 100000; k++) {
  pi += 4 * Math.pow(-1, k + 1) / (2 * k - 1);
  $("#result").text(pi);
  $("#count").text(k);
 }
}

where the DOM elements are provided by:

<div id="result">
0
</div>
<div id="count">
0
</div>
<button id="go">Go</button>

The intention is to display the progress of the calculation by changing the text displayed in the two divs each time through the for loop.  If you try it out what you will find is that the UI freezes for some minutes and nothing is displayed in the web page until the loop finishes and the UI thread is freed to tend to the UI. 

To keep the UI responsive, and in this case to see the intermediate results, we have to turn the calculation into an asynchronous function using setTimeout. We do this by breaking the calculation in small chunks - say 1000 iterations each. To do this we need a state object that records the state of the calculation so that it can be resumed:

var state = {};
state.k = 0;
state.pi = 0;

The function now performs 1000 iterations and then updates the text in the divs. To enable the UI to stay responsive, the function then terminates but not before setting itself up in the event queue ready to perform another 1000 iterations after the UI has been up-dated:

function computePi() {
 if (state.k >= 100000000) return;
 var i;
 for (i = 0; i < 1000; i++) {
  state.k++;
  state.pi +=
    4 * Math.pow(-1, state.k + 1) / (2 * state.k - 1);
 }
 $("#result").text(state.pi);
 $("#count").text(state.k);
 setTimeout(computePi, 0);
} 

If you run this version of the computation you will find that not only does the UI remain responsive you get to see the intermediate values as the calculation proceeds. Notice that the computePi function is now almost asynchronous in that it returns after doing 1000 iterations. You can change it to be truly async by for example using a function to setTimeout for the first call and return. That is:

function computePiAsync(){
 setTimeout(computePi, 0);
}

Notice also that the function is able to access the state object because it is a global variable. If you don't want to use a global variable then create the computePi function inside another function and rely on closure to make the state variable accessible:

function computePiAsync(){
 var state = {};
 state.k = 0;
 state.pi = 0;
 function computePi() {
  if (state.k >= 100000000) return;
  var i;
  for (i = 0; i < 1000; i++) {
   state.k++;
   state.pi +=
     4 * Math.pow(-1, state.k + 1)/(2 * state.k - 1);
  }
  $("#result").text(state.pi);
  $("#count").text(state.k);
  setTimeout(computePi, 0);
 }
 setTimeout(computePi, 0);
};

You can use the same technique to turn nearly any long running computation into an asynchronous procedure. All you have to do is break the computation down into small parts and preserve the state of the computation at the end of each chunk so that it can be restarted. Write the function so that it takes the state object and continues the computation. This is always possible, even if the task isn't to sum a mathematical series. For example, if you want to perform a complex database operation simply save the point in the transaction that you have reached. 



Last Updated ( Thursday, 05 May 2022 )