JavaScript Async - The Callback & The Controller
Written by Ian Ellliot   
Monday, 31 December 2018
Article Index
JavaScript Async - The Callback & The Controller
The Problem With Async
Using The Controller

 

The Problem With Asynchronous

Callbacks may be like event handlers, but the way that they occur in code makes them a very different proposition. You might be able to implement callbacks as events, but they have some important differences.

Event handlers are in the main easy to write and cause few problems, but callbacks have some serious problems. The reason for this difference is that event handlers are set up by code that has no interest in their results.

Event handlers generally don't do anything that the code that sets them up cares about.

When you set a button's click handler, the code that sets it generally doesn't want to interact with the click handler later in time when a click occurs. It's a set and forget operation. In other words, event handlers are usually fairly closed pieces of code that don't depend on anything other than the event that just occurred and the global state of the program.

Callbacks, on the other hand, generally do something that the code that sets them up cares about.

Generally you want to download a file and process the data it contains. In an ideal world the download would complete and then the code would continue on its way to process the file. When you have to use a callback this isn't the way it happens – the process is initiated and the code that started it comes to an end; only later does the callback activate and the process is completed elsewhere.

There is a real sense in which a callback is a continuation of the program that called the non-blocking function, whereas an event handler isn't a continuation of any particular part of the program.

This is more difficult.

You will hear lots of explanations of the problem of asynchronous code along the lines of "callback hell", and the "callback pyramid of doom". These are problems, but they arise from taking particular approaches to asynchronous programming.

The first problem is that raw asynchronous programming distorts the intended flow of control.

In a synchronous program you might write:

loadA();
loadB();
loadC();

and you can expect A to load before B which loads before C.

As soon as you convert these to async operations you can't be sure what order things are done in unless you adopt the callback cascade:

loadA(loadB(loadC()));

where each function accepts a callback as its parameter. That is, loadA calls loadB as soon as its file is downloaded and then loadB calls loadC when its file is downloaded. Each callback determines what is to happen next.

The callback approach to async turns sequential default flow of control into nested function calls.

But keep in mind that the callback approach is just one of many. Because it is so widely used, there is a tendency to think that a callback is the only way to deal with asynchronous code. As we have already seen, we can avoid callbacks by implementing events that are fired when the non-blocking function has finished its task and the program can continue from where it left off.

Before you conclude that using events instead of callbacks is a better way of doing things, notice that events convert the sequential flow of control into nested events.

For example, assuming that we have objects loadA, loadB and loadC that fire the loaded event when their get method completes:

loadA.addEventListener("loaded",
function(fileA){loadB.get()}
loadB.addEventListener("loaded",
function(fileB){loadC.get()}
loadC.addEventListener("loaded",
function(fileC){...}
loadA.get()

Which isn't a lot better than the nested callbacks. In this case the event handlers are set up first and then the first file load is started. This triggers the loaded event which in turn starts the second file loading. In the real world each event handler would also process the file that had been loaded. This is just as much a distortion of the sequential flow of control that we are trying to implement as a callback.

Notice that the sequential flow of control is the simplest and things become much more difficult if you try to implement conditionals or loops which involve non-blocking functions.

Put simply the need to yield the UI thread before the task is complete is a major disruption to the flow of control.

Asynchronous Flow of Control and Closure

  Final version in book

Asynchronous Errors

 Final version in book

Controlling Callback Flow of Control – Sequential

As we have already seen you can implement a sequential flow of control using callbacks. All you have to do is place a call to the next operation in each callback.

This works, but there are a number of techniques that you can use to simplify or automate the sequential calling of non-blocking functions with callbacks.

All of these make use of a controlling object or sequencer. For example, jQuery has its function queue which can be used to execute one non-blocking task after another. The problem with all of these schemes is that they are either limited or become increasingly complex to cope with more general situations. However, this said, for simple situations they are worth considering as alternatives to more sophisticated solutions such as Promises.

The key idea is that you make use of a controller object which provides methods to manage the execution of the non-blocking functions. In general the object has an add method to allow you to add a function to the queue, and a method to start execution of the queue.

For example we can define a simple controller object with an add method:

var controller = {};
controller.queue= [];
controller.add = function (func) { 
controller.queue.push(func);
};

In this case the controller object is defined as a singleton, but you can create a constructor if you need multiple instances. You can also use the standard techniques to make the queue private.

The controller has an array that is used as a FIFO queue. The add method simply adds the non-blocking function to the queue.

If we assume that each non-blocking function accepts a single parameter which is the callback we can start things moving using:

controller.run = 
            function () {
controller.queue.shift()(controller.run);
};

This takes the function on the head of the queue and runs it passing the run method as the callback. This means that when each non-blocking function ends it starts the next function going, as its callback is the run method which always runs the next function in the queue.

<ASIN:1871962560>

<ASIN:1871962579>



Last Updated ( Monday, 31 December 2018 )