jQuery 3 - Function Queues
Written by Ian Elliot   
Monday, 30 January 2017
Article Index
jQuery 3 - Function Queues
The Async Problem
Chained Auto Queuing

The Async Problem

There are lots of examples of asynchronous functions in JavaScript. The best known is probably the Ajax call but downloading almost any file is generally executed in an asynchronous fashion. For simplicity we are going to use the setTimeout() function to simulate an asynchronous function. To remind you 

setTimeout(function,milliseconds);

will run the specified function after a delay of the specified number of milliseconds. This really does run the function asynchronously because it adds a message to the event queue to run the function after the specified time and returns at once. It is also the basic way to create an asynchronous function that does some useful work while allowing the UI to run. 

To convert the example functions to asynchronous operation we simply move the log output into a separate function that runs after a few seconds:

function myF1() {
 setTimeout(function(){console.log("myF1");},3000);
 $(this).dequeue("myQueue");
}
function myF2() {
 setTimeout(function(){console.log("myF2");},2000);  $(this).dequeue("myQueue");
}
function myF3() {
 setTimeout(function(){console.log("myF3");},1000);  $(this).dequeue("myQueue");
}

Now we have myF1 which prints myF1 on the log after waiting 3 seconds, myF2 which prints myF2 on the log after waiting 2 seconds and myF3 which prints myF3 onto the log after waiting 1 second. 

If these functions are queued in the usual way and dequeued then what happens might surprise you but it shouldn't. When myF1 is dequeued it sets an asynchronous call going which concludes after 3 seconds but it returns at once i.e. before the asynchronous function has completed. Then the dequeue causes myF2 to execute and it adds an asynchronous function that executes after 2 seconds and immediately dequeues the next function i.e. myF3 which adds an asynchronous function which executes after 1 second. The result is that the queue adds each of the asynchronous functions almost at the same time and hence myF3 is first to print on the log, then myF2 and finally myF1. 

Notice that you get the same result if you simply call each of the functions in turn. That is

myF1();
myF2();
myF3();

Each function returns at once after placing an asynchronous function on the event queue. The result is the same and myF3 is printed to the log first followed by myF2 and myF1.

This might be what was intended but if the intent was to execute myF2 after myF1 has completely finished including any asynchronous portions and myF3 after myF2 has completely finished then this is not what is happening. 

The simple solution is to remember that the dequeue function can be thought of as a single to the rest of the program that the function has finished. Currently the dequeue function is at the end of each of the functions but this does not mark the end of what the function does - clearly the call to dequeue should be at the end of the asynchronous part of the function. However you can't just move the call to the asynchronous function as in:

function myF1() {
  setTimeout(function () {
    console.log("myF1");
    $(this).dequeue("myQueue");
  }, 3000); }

The reason this doesn't work is that by the time the asynchronous function is called this no longer references the object that hosts the queue. To get round this the simplest solution is to save this in another variable and then rely on closure to give the asynchronous function access to it. 

That is if you change the three functions to:

function myF1() {
 var that = this;
 setTimeout(function () {
   console.log("myF1");
   $(that).dequeue("myQueue");
  }, 3000);
}

function myF2() {
 var that = this;
 setTimeout(function () {
   console.log("myF2");
   $(that).dequeue("myQueue");
 }, 3000);
}

function myF3() {
 var that = this;
 setTimeout(function () {
   console.log("myF3");
   $(that).dequeue("myQueue");
 }, 3000);
}

Everything works as you would expect. Now when you dequeue for the first time the myF1 function is executed but its dequeue isn't called until 3000 seconds are up and after myF1 has been printed to the log. Thus myF2 runs after myF1 is completely finished and it only calls dequeue after it has completely finished. What this means is that after 3 seconds myF1 is printed, then after a further 2 seconds myF2 is printed and finally after a further 1 second myF3 is printed. 

You can see how placing the dequeue instructions to indicate that the function has completely finished including any asynchronous portion you can ensure that the functions in the queue are executed one after the other. 

Of course you can implement modified versions of this scheme that might allow some asynchronous functions to overlap slightly or even completely. You just have to keep in mind that when you call the dequeue function the next function in the queue gets to start. 

This is what function queues are all about and there is nothing more to learn apart from some small extras that can make things easier. 

The Next Parameter

The need to preserve this to pass to any asynchronous portion of the function is a complication. Since jQuery 1.4 the function queue has passed the next function in the queue as the first argument to the function currently being dequeued. This makes it possible to call the next function and dequeue it without having to keep a reference to the object that the queue is hosted by. 

For example we could write any of the function given earlier as:

function myF1(next) {
 setTimeout(function () {
   console.log("myF1");
   next();
 }, 3000);
}

The call to next dequeues the next function and executes it. Notice that you don't need to pass next to the asynchronous function because it is accessible as part of a closure.

The Complete Listing

The complete listing of the program so far including the use of the next parameter is:

var obj = $({}).queue("myQueue", myF1)
                .queue("myQueue", myF2)
                 .queue("myQueue", myF3); obj.dequeue("myQueue");

function myF1(next) { 
 setTimeout(function () {
   console.log("myF1");
   next();
 }, 3000);
}

function myF2(next) { 
 setTimeout(function () {
   console.log("myF2");
   next();
 }, 2000);
}

function myF3(next) { 
 setTimeout(function () {
   console.log("myF3");
   next();
 }, 1000);
}

 

Adding Functions To jQuery

Functions that work with queues are most easily added to jQuery so that they can be used as if they were native jQuery functions. This is how the animation function, see the next chapter, work and it is worth knowing how to add new functions. 

All you have to do to add a function to jQuery is use the fn.extend function:

$.fn.extend(object);

This works much like the extend function but it adds all of the properties of the object to the jQuery prototype. Notice that this means that every jQuery object acquires the new properties including any you might have already created. 

To add functions to jQuery all you have to do is add properties to the object that are functions. If you want to use function chaining, which is the usual style of using functions in jQuery then you need to make sure that the functions you define return this. The reason is that this is set to the call context i.e. the current jQuery object and this needs to be what the next function in the chain uses as its call context. 

So for example to add our example functions to jQuery and allow them to be called in a chained style we need:

$.fn.extend({
 myF1: function () {
         setTimeout(function () {
            console.log("myF1");
         }, 3000);
         return this;
       },
 myF2: function () {
         setTimeout(function () {
            console.log("myF2");
         }, 2000);
         return this;
       },
 myF3: function () {
         setTimeout(function () {
            console.log("myF3");
         }, 1000);
         return this;
       }
});

After this we can call the three functions using:

$({}).myF1().myF2().myF3();

Notice that our three functions don't make use of the function queue as a result each one adds its asynchronous part to the event queue almost at the same time. This means that we see in the log myF3, myF2 and myF1 which might well not be what we want. 

If we want the asynchronous chained functions to execute in the order written we need to use a queue.



Last Updated ( Monday, 30 January 2017 )