An in depth understanding of synchronous and asynchronous mechanisms in JavaScript programming

  • 2020-06-19 09:44:13
  • OfStack

One advantage of JavaScript is how it handles asynchronous code. Asynchronous code is put into an event queue and waits for all other code to execute without blocking the thread. However, writing asynchronous code can be difficult for beginners. And in this article, I will dispel any confusion you might have.
Understanding asynchronous code

The most basic asynchronous functions of JavaScript are setTimeout and setInterval. setTimeout executes the given function at a fixed time of 1. It takes a callback function as argument 1 and a millisecond as argument 2. Here are some examples:


console.log( "a" );
setTimeout(function() {
  console.log( "c" )
}, 500 );
setTimeout(function() {
  console.log( "d" )
}, 500 );
setTimeout(function() {
  console.log( "e" )
}, 500 );
console.log( "b" );

As expected, the console prints "a", "b", and after about 500 milliseconds, "c", "d", "e". I use "approximately" because setTimeout is actually unpredictable. In fact, even the HTML5 specification addresses this issue:

"There is no guarantee that the API timing will work exactly as planned. Delays due to the CPU load, other tasks, etc., are expected."


Interestingly, the timeout does not occur until all the remaining code in the same 1 program segment has finished executing. So if a timeout is set and a long-running function is executed, the timeout won't even start until the function completes. In fact, asynchronous functions, such as setTimeout and setInterval, are pressed into queues called Event Loop.

Event Loop is a queue of callback functions. When the asynchronous function executes, the callback function is pressed into the queue. The JavaScript engine does not begin processing the event loop until the asynchronous function has completed execution. This means that the JavaScript code is not multithreaded, even though it behaves similarly. The event loop is a first in first out (FIFO) queue, indicating that the callbacks are executed in the order in which they were queued. JavaScript was chosen as the development language by node because it was so easy to write such code.

Ajax

Asynchronous Javascript and XML (AJAX) permanently change the state of the Javascript language. Suddenly, the browser doesn't need to reload to update the web page. The code to implement Ajax in different browsers can be long and tedious; However, thanks to jQuery (and other libraries), we can implement client-server communication in an easy and elegant way.

We could easily retrieve the data using the jQuery cross-browser interface $.ajax, but we couldn't render what was going on behind the scenes. Such as:


var data;
$.ajax({
  url: "some/url/1",
  success: function( data ) {
    // But, this will!
    console.log( data );
  }
})
// Oops, this won't work...
console.log( data );

An easy mistake to make is to use data immediately after calling $.ajax, but it actually looks like this:


xmlhttp.open( "GET", "some/ur/1", true );
xmlhttp.onreadystatechange = function( data ) {
  if ( xmlhttp.readyState === 4 ) {
    console.log( data );
  }
};
xmlhttp.send( null );

The underlying XmlHttpRequest object initiates the request and sets up the callback function to handle readystatechnage events for XHR. Then execute send's send method. During the XHR run, the readystatechange event is triggered when its attribute readyState changes, and the callback function only fires when XHR receives the response from the remote server.

Handling asynchronous code

Asynchronous programming can easily fall into what we call callback hell. Since virtually all asynchronous functions in JS use callbacks, the result of executing several asynchronous functions in a row is a series of nested callbacks and the complex code that comes with them.

Many of the functions in node.js are also asynchronous. So the following code is basically common:


var fs = require( "fs" );
fs.exists( "index.js", function() {
  fs.readFile( "index.js", "utf8", function( err, contents ) {
    contents = someFunction( contents ); // do something with contents
    fs.writeFile( "index.js", "utf8", function() {
      console.log( "whew! Done finally..." );
    });
  });
});
console.log( "executing..." );

See also the following client code:



GMaps.geocode({
  address: fromAddress,
  callback: function( results, status ) {
    if ( status == "OK" ) {
      fromLatLng = results[0].geometry.location;
      GMaps.geocode({
        address: toAddress,
        callback: function( results, status ) {
          if ( status == "OK" ) {
            toLatLng = results[0].geometry.location;
            map.getRoutes({
              origin: [ fromLatLng.lat(), fromLatLng.lng() ],
              destination: [ toLatLng.lat(), toLatLng.lng() ],
              travelMode: "driving",
              unitSystem: "imperial",
              callback: function( e ){
                console.log( "ANNNND FINALLY here's the directions..." );
                // do something with e
              }
            });
          }
        }
      });
    }
  }
});

Nested callbacks can get really nasty, but there are several solutions to this style of coding.

It's easy to get a "bad taste" in your code with nested callbacks, but there are several styles you can try to solve this problem

The problem isn t with the language itself; it 'with the way programmers the language -- Async Javascript.

No bad language, only bad programming apes - asynchronous JavaSript


Naming functions

A handy solution for clearing nested callbacks is to simply avoid nesting above two layers. Pass a named function as a callback argument instead of passing an anonymous function:


var fromLatLng, toLatLng;
var routeDone = function( e ){
  console.log( "ANNNND FINALLY here's the directions..." );
  // do something with e
};
var toAddressDone = function( results, status ) {
  if ( status == "OK" ) {
    toLatLng = results[0].geometry.location;
    map.getRoutes({
      origin: [ fromLatLng.lat(), fromLatLng.lng() ],
      destination: [ toLatLng.lat(), toLatLng.lng() ],
      travelMode: "driving",
      unitSystem: "imperial",
      callback: routeDone
    });
  }
};
var fromAddressDone = function( results, status ) {
  if ( status == "OK" ) {
    fromLatLng = results[0].geometry.location;
    GMaps.geocode({
      address: toAddress,
      callback: toAddressDone
    });
  }
};
GMaps.geocode({
  address: fromAddress,
  callback: fromAddressDone
});

In addition, the ES129en. js library helps us handle multiple Ajax requests/responses. For example:


async.parallel([
  function( done ) {
    GMaps.geocode({
      address: toAddress,
      callback: function( result ) {
        done( null, result );
      }
    });
  },
  function( done ) {
    GMaps.geocode({
      address: fromAddress,
      callback: function( result ) {
        done( null, result );
      }
    });
  }
], function( errors, results ) {
  getRoute( results[0], results[1] );
});

This code executes two asynchronous functions, each receiving a callback function named "done" and calling it at the end of the function. When the two "done" callbacks end, the callback function of the parallel function is called and executes or handles the result or error produced by the two asynchronous functions.

Promises model
Quotations from CommonJS/A:

promise represents the final result returned when 1 operation completes independently.

There are many libraries that contain the promise model, and jQuery already has a working and excellent promise API. jQuery introduced Deferred objects in version 1.5 and can use the construction results of ES158en.Deferred in functions that return promise. The function that returns promise, on the other hand, is used to perform some asynchronous operation and resolve the delay after completion.


var geocode = function( address ) {
  var dfd = new $.Deferred();
  GMaps.geocode({
    address: address,
    callback: function( response, status ) {
      return dfd.resolve( response );
    }
  });
  return dfd.promise();
};
var getRoute = function( fromLatLng, toLatLng ) {
  var dfd = new $.Deferred();
  map.getRoutes({
    origin: [ fromLatLng.lat(), fromLatLng.lng() ],
    destination: [ toLatLng.lat(), toLatLng.lng() ],
    travelMode: "driving",
    unitSystem: "imperial",
    callback: function( e ) {
      return dfd.resolve( e );
    }
  });
  return dfd.promise();
};
var doSomethingCoolWithDirections = function( route ) {
  // do something with route
};
$.when( geocode( fromAddress ), geocode( toAddress ) ).
  then(function( fromLatLng, toLatLng ) {
    getRoute( fromLatLng, toLatLng ).then( doSomethingCoolWithDirections );
  });

This allows you to execute two asynchronous functions, wait for their results, and then use the results of the previous two calls to execute another function.

promise represents the final result returned when 1 operation completes independently.

In this code, the geocode method executes twice and returns 1 promise. The asynchronous function then executes and calls resolve in its callback. Then, once the two calls to resolve are complete, then will execute, receiving the return result of the previous two calls to geocode. The result is then passed in getRoute, and this method also returns 1 promise. Finally, when getRoute's promise is resolved, the doSomethingCoolWithDirections callback is executed.

The event
Event is another way to communicate when the asynchronous callback is finished processing. One object can become an emitter and dispatch events, while another listens for them. This type of event handling is called the observer pattern. backbone. js library creates such functional modules in ES185en. Events.


var SomeModel = Backbone.Model.extend({
  url: "/someurl"
});
var SomeView = Backbone.View.extend({
  initialize: function() {
    this.model.on( "reset", this.render, this );
    this.model.fetch();
  },
  render: function( data ) {
    // do something with data
  }
});
var view = new SomeView({
  model: new SomeModel()
});

There are other hybrid examples and libraries for launching events, such as jQuery Event Emitter, EventEmitter, monologue. js, and node. js's built-in EventEmitter module.

An event loop is a queue of 1 callback function.

A similar way of distributing messages is called the mediator pattern, which is used in the ES203en. js library. In the intermediary mode, there is a middleman for all objects to listen to and dispatch events. In this mode, an object is not directly related to another object, thus the objects are separated from each other.

Never return promise to a common API. This not only affects API users' use of promises, but also makes refactoring more difficult. However, the combination of promises for internal use and events for external interfaces makes the application less coupled and easier to test.

In the previous example, the doSomethingCoolWithDirections callback function was executed after the completion of two geocode functions. doSomethingCoolWithDirections then receives the response received from getRoute and sends it out as a message.


var doSomethingCoolWithDirections = function( route ) {
  postal.channel( "ui" ).publish( "directions.done", {
    route: route
  });
};

This allows the rest of the application to respond to asynchronous callbacks without directly referencing the object that generated the request. When you get the command, it's likely that many areas of the page will need to be updated. In a typical jQuery Ajax process, a smooth callback may have to be adjusted as the command received changes. This may make the code difficult to maintain, but with messages, it is much easier to handle updates to multiple regions of UI.


var data;
$.ajax({
  url: "some/url/1",
  success: function( data ) {
    // But, this will!
    console.log( data );
  }
})
// Oops, this won't work...
console.log( data );

0

Other libraries that transmit messages based on the mediator pattern are amplify, PubSubJS, and ES234en.js.

conclusion

JavaScript makes it easy to write asynchronous code. Use promises, events, or named functions to avoid "callback hell". For more information on asynchronous programming, click Async JavaScript: Build More with Less.


Related articles: