how to use q.js promises to work with multiple asynchronous operations

Kim Stacks picture Kim Stacks · Nov 28, 2012 · Viewed 35.9k times · Source

Note: This question is also cross-posted in Q.js mailing list over here.


i had a situation with multiple asynchronous operations and the answer I accepted pointed out that using Promises using a library such as q.js would be more beneficial.

I am convinced to refactor my code to use Promises but because the code is pretty long, i have trimmed the irrelevant portions and exported the crucial parts into a separate repo.

The repo is here and the most important file is this.

The requirement is that I want pageSizes to be non-empty after traversing all the dragged'n dropped files.

The problem is that the FileAPI operations inside getSizeSettingsFromPage function causes getSizeSettingsFromPage to be async.

So I cannot place checkWhenReady(); like this.

function traverseFiles() {
  for (var i=0, l=pages.length; i<l; i++) {
    getSizeSettingsFromPage(pages[i], calculateRatio);   
  }
  checkWhenReady(); // this always returns 0.
}

This works, but it is not ideal. I prefer to call checkWhenReady just ONCE after all the pages have undergone this function calculateRatio successfully.

function calculateRatio(width, height, filename) {
  // .... code 
  pageSizes.add(filename, object);
  checkWhenReady(); // this works but it is not ideal. I prefer to call this method AFTER all the `pages` have undergone calculateRatio
  // ..... more code...
}

How do I refactor the code to make use of Promises in Q.js?

Answer

Nathan Wall picture Nathan Wall · Nov 28, 2012

My suggestions to get this working with Q.js are below. The key is that anytime you want to do something asynchronously, you should return a promise, and once the task is completed you should resolve that promise. That allows the callers of the function to listen for the task to be completed and then do something else.

As before, I have commented my changes with // ***. Let me know if you have any further questions.

        function traverseFiles() {
            // *** Create an array to hold our promises
            var promises = [ ];
            for (var i=0, l=pages.length; i<l; i++) {
                // *** Store the promise returned by getSizeSettingsFromPage in a variable
                promise = getSizeSettingsFromPage(pages[i]);
                promise.then(function(values) {
                    var width = values[0],
                        height = values[1],
                        filename = values[2];
                    // *** When the promise is resolved, call calculateRatio
                    calculateRatio(width, height, filename);
                });
                // *** Add the promise returned by getSizeSettingsFromPage to the array
                promises.push(promise);
            }
            // *** Call checkWhenReady after all promises have been resolved
            Q.all(promises).then(checkWhenReady);
        }

        function getSizeSettingsFromPage(file) {
            // *** Create a Deferred
            var deferred = Q.defer();
            reader = new FileReader();
            reader.onload = function(evt) {
                var image = new Image();
                image.onload = function(evt) {
                    var width = this.width;
                    var height = this.height;
                    var filename = file.name;
                    // *** Resolve the Deferred
                    deferred.resolve([ width, height, filename ]);
                };
                image.src = evt.target.result;
            };
            reader.readAsDataURL(file);
            // *** Return a Promise
            return deferred.promise;
        }

Edit

defer creates a Deferred, which contains two parts, a promise and the resolve function. The promise is returned by getSizeSettingsFromPage. Basically returning a promise is a way for a function to say "I'll get back to you later." Once the function has completed it's task (in this case once the image.onload event has fired) the resolve function is used to resolve the promise. That indicates to anything waiting on the promise that the task has been completed.

Here's a simpler example:

function addAsync(a, b) {
    var deferred = Q.defer();
    // Wait 2 seconds and then add a + b
    setTimeout(function() {
        deferred.resolve(a + b);
    }, 2000);
    return deferred.promise;
}

addAsync(3, 4).then(function(result) {
    console.log(result);
});
// logs 7 after 2 seconds

The addAsync function adds two numbers but it waits 2 seconds before adding them. Since it's asynchronous, it returns a promise (deferred.promse) and resolves the promise after the 2 second wait (deferred.resolve). The then method can be called on a promise and passed a callback function to be executed after the promise has been resolved. The callback function is passed in the resolution value of the promise.

In your case, we had an array of promises and we needed to wait for all of them to be done before executing a function, so we used Q.all. Here's an example:

function addAsync(a, b) {
    var deferred = Q.defer();
    // Wait 2 seconds and then add a + b
    setTimeout(function() {
        deferred.resolve(a + b);
    }, 2000);
    return deferred.promise;
}

Q.all([
    addAsync(1, 1),
    addAsync(2, 2),
    addAsync(3, 3)
]).spread(function(result1, result2, result3) {
    console.log(result1, result2, result3);
});
// logs "2 4 6" after approximately 2 seconds