Mobile Zone is brought to you in partnership with:

Troy Miles is a Southern California based software developer. He began by writing games for the Apple II, C64 and IBM PC in assembly language over 30 years ago. After burning out on games he switch to creating windows application software in C and C++. And now he develops applications for both web, mobile web and mobile devices in C#, Objective C, Java, and JavaScript. He is also a big fan of jQuery, jQuery Mobile and PhoneGap. Troy is a DZone MVB and is not an employee of DZone and has posted 27 posts at DZone. You can read more from them at their website. View Full User Profile

jQuery Deferred Object, Part 2: How to Create Your Own

11.11.2013
| 3121 views |
  • submit to reddit
In my last post, jQuery Deferred Object - Your New Best Friend, I gave a quick overview of what a deferred object is and how to use it. This time let's go a bit deeper and explore creating your own deferred object for a much more practical use - asynchronous downloading and processing of audio files.

One of my favorite web sites is HTML5Rocks. They have all of the latest information on developing cutting edge HTML5 web apps. As I continue building my HTML5 game, I've turned to them for more information on several topics including the Web Audio API. In one of their articles,  Getting Started with Web Audio API, they create a BufferLoader class.

function BufferLoader(context, urlList, callback) {
  this.context = context;
  this.urlList = urlList;
  this.onload = callback;
  this.bufferList = new Array();
  this.loadCount = 0;
}

BufferLoader.prototype.loadBuffer = function(url, index) {
  // Load buffer asynchronously
  var request = new XMLHttpRequest();
  request.open("GET", url, true);
  request.responseType = "arraybuffer";

  var loader = this;

  request.onload = function() {
    // Asynchronously decode the audio file data in request.response
    loader.context.decodeAudioData(
      request.response,
      function(buffer) {
        if (!buffer) {
          alert('error decoding file data: ' + url);
          return;
        }
        loader.bufferList[index] = buffer;
        if (++loader.loadCount == loader.urlList.length)
          loader.onload(loader.bufferList);
      },
      function(error) {
        console.error('decodeAudioData error', error);
      }
    );
  }

  request.onerror = function() {
    alert('BufferLoader: XHR error');
  }

  request.send();
}

BufferLoader.prototype.load = function() {
  for (var i = 0; i < this.urlList.length; ++i)
    this.loadBuffer(this.urlList[i], i);
}
I thought it would be fun to convert this class to use a deferred object and streamline it even more by getting rid of the object constructor and instead use a closure. So let's get started.

Getting Rid of the Constructor

The BufferLoader uses a function as an object creator. There is nothing wrong with that per se, it is just a pattern which I rarely use these days. Instead I normally prefer to use a closure. Think about why they are creating an object. Since both the loading and the converting of the files are asynchronous operations they want to be to support multiple calls into the same code. Using a constructor will guarantee that each instance has access to its own set of variables and won't interfere with any other call. A closure does the same thing, but more succinctly. So we will delete the constructor function. We also will wrap our entire code in a function to isolate from any other code in the browser. We use a single global object, RocknCoder, to hold all of our global stuff. We make the loadBuffer() method an internal method of the new RocknCoder.loadAudioFiles() method. This also makes it accessible globally.

Creating the Closure

Most of the instance variables are moved or passed into the loadAudioFiles() method. The this variable or its alternate name, loader, is no longer necessary, so we delete it to. All of its uses become the name without the "loader.", for example "loader.context" becomes simply "context". I don't know about you, but too much using of "this" in JavaScript, gets really confusing. To create the closure we call the loadBuffer method. When this method is called, it essentially creates a snapshot of the environment, all of the variables and their values become frozen, so that when an async callback happens, the state of the variables is restored. Note the subtle change of the parameters passed to loadBuffer(). Previously it was passed a single URL and index which was to the bufferList[] array. Now it is passed all of the URLs as an array and an index value which indicates which URL is currently being loaded. Once a URL is loaded and processed, index is incremented and if we haven't loaded all of the URLs yet, loadBuffer() is called recursively. Do it this way also eliminates the need for load() function, so we delete it too.

Making Use of the Deferred Object

Right off the back in the loadAudioFile() method, we create our deferred object by calling the jQuery $.Deferred() method. The last line of the method returns the deferred object, myDeferred to the caller. Keep mind that this method, will continue to run even after it returns to the caller since their are multiple asynchronous callback going on here. In the method we curiously use the XMLHttpRequest object instead of using jQuery's version of it. This is mainly because the XMLHttpRequest object supports the "arraybuffer" type which allows us to load binary data, very important for audio.

If we encounter an error along the way we can call the reject() method and pass some error information all with it. This will cause our deferred object to fail and the information passed to it will be available to the fail() method.

Once all of our audio files has loaded and been processed, we simply resolve() our deferred object. In this case we pass our bufferList, which contains all of our processed audio files to our deferred object. This will make them available to our done() method. Also note how we eliminated the need to track the index count on bufferList by using the push() method instead an index.

We could have also used the deferred object's progress() method each time we successfully finished either loading or processing a file. That would allow the caller to update a UI element to keep the user informed as to our progress. Oh well, maybe next time.

/**
 * User: Troy
 * Date: 11/4/13
 * Time: 4:24 AM
 */


var RocknCoder = RocknCoder || {};

(function () {
  "use strict";

  /*
   The loadAudioFiles method uses jQuery deferred object,
   but not its ajax loader since it doesn't support binary data (arraybuffer)
   once it has loaded all of the audio files, it will resolve() itself and
   pass the audio data to the done method
   */
  RocknCoder.loadAudioFiles = function(context, urlList) {
    var myDeferred = $.Deferred(),
      len = urlList.length,
      bufferList = [],
      loadBuffer = function (urls, index) {
        // Load buffer asynchronously
        var request = new XMLHttpRequest();

        request.open("GET", urls[index], true);
        request.responseType = "arraybuffer";
        request.onload = function () {
          // Asynchronously decode the audio file data in request.response
          context.decodeAudioData(
            request.response,
            function (buffer) {
              if (!buffer) {
                myDeferred.reject('error decoding file data: ' + urls[index]);
              } else {
                bufferList.push(buffer);
                if (++index === len) {
                  myDeferred.resolve(bufferList);
                } else {
                  loadBuffer(urls, index);
                }
              }
            },
            function (error) {
              myDeferred.reject('decodeAudioData error', error);
            }
          );
        }
        // if there is some kind of loading error come here
        request.onerror = function () {
          myDeferred.reject('unknown error occurred');
        }

        // begin the download
        request.send();
      };

    loadBuffer(urlList, 0);
    return myDeferred;
  };
}());

Summary

We started with roughly 46 lines of code in three methods and converted into roughly 42 lines in one method. Doesn't seem like too great of a win but the code is cleaner to read and most of the win is for the caller. Instead of having to deal with a single callback, where they have to sort out the results. They now get separate done() and fail() methods. Plus the deferred object passed here can be combined with others in a when() statement. An example of this is in the game I am building. We wait for all of the audio files, the sprite map, and a minimum of three seconds to pass before we leave the splash screen. An example of which is below.

If you would like to see more of the game, be sure to check out my repo on GitHub at: https://github.com/Rockncoder/planegame. Be sure to check out the first post of this series: jQuery Deferred Object - Your New Best Friend.

  RocknCoder.Pages.splash = (function () {
    return {
      pageshow: function () {
        RocknCoder.Game.dims = RocknCoder.Dimensions.get();

        var context, loaderReady,
          timerReady = $.Deferred(),
          imageReady = $.Deferred(),
          sounds = [
            "sounds/83560__nbs-dark__ship-fire.wav",
            "sounds/95078__sandyrb__the-crash.wav",
            "sounds/143611__d-w__weapons-synth-blast-01.wav",
            "sounds/DST-Afternoon.mp3"
          ];

        try {
          // Fix up for prefixing
          window.AudioContext = window.AudioContext || window.webkitAudioContext;
          context = new AudioContext();
          console.log('Web Audio API is supported in this browser');
          loaderReady = RocknCoder.loadAudioFiles(context, sounds);
        }
        catch (e) {
          console.log('Web Audio API is NOT supported in this browser');
        }

        RocknCoder.Game.spriteSheet = new Image();
        RocknCoder.Game.spriteSheet.src = "images/1945.png";
        RocknCoder.Game.spriteSheet.onload = function () {
          imageReady.resolve();
        };

        // our timer simply waits until it times out, then sets timerReady to resolve
        setTimeout(function () {
          timerReady.resolve();
        }, 3000);

        // put our load screen up
        $.mobile.loading( 'show', {
          text: "Loading resources...",
          textVisible: true,
          theme: "a"
        });

        $.when(loaderReady, timerReady, imageReady)
          .done(function (loaderResponse) {
            // let's put the data in our global
            RocknCoder.Resources = RocknCoder.Resources || {};
            RocknCoder.Resources.audios = loaderResponse;
          })
          // here you would check to find out what failed
          .fail(function () {
            console.log("An ERROR Occurred")
          })
          // the always method runs whether or not there were errors
          .always(function () {
            $.mobile.loading("hide");
            $.mobile.changePage("#attract");
          });
      },
      pagehide: function () {
      }
    };
  }());


Published at DZone with permission of Troy Miles, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)