Jay
Jay

Reputation: 183

How to avoid hard-coded, chained asynchronous functions in Javascript/jQuery?

Almost all of the functions in my program have some sort of asynchronous call, but they all rely on some previous function's results. Because of that, I've hard-coded the next function call into each individual one as such:

function getStuff() {
    $.ajax({
        ...
        success: function(results) {
            // other functions involving results
            getMoreStuff(results);
        }
    });
}

function getMoreStuff(results) {
    $.ajax({
        ...
        success: function(moreResults) {
            // other functions involving moreResults
            doSomethingWithStuff(moreResults);
        }
    );
}

And so on. It's a large chain where each function calls the next. While this works within the program, it makes each function useless individually.

I'm a bit lost on how to avoid this problem. I couldn't figure out how to use general callback functions, because when I make the function calls, it ends up like this (using the functions above):

getStuff(function() {
    getMoreStuff(results, doSomethingWithStuff);
};

But then 'results' hasn't been defined yet.

The solution seems obvious, I'm just being a bit dense about it. Sorry!

Upvotes: 11

Views: 673

Answers (4)

Maciej Pyszyński
Maciej Pyszyński

Reputation: 9374

The solution is very simple. You have to use Publish–subscribe pattern. The easiest implementation with jQuery:

$('body').trigger('joined-game', [game_id, response]);

First argument is the event name which you're publishing, second argument is the data array.

The best practise is to trigger event on the most specific DOM element, but if you subscribe the same event on multiple pages, and aren't sure if DOM element is present on all pages you can trigger it on body or some "dump/synthetic" not visible DOM element always present on all pages.

$("body").on('joined-game', function(event, game_id, response){
    //...
});

Then you subscribe event you want to utilize. Remember that besides your data, first argument is always event.

Another advantage of this solution is that you can split your code to several files.

More details: http://webility.pl/en/blog/entry/chaining-javascript-functions-without-dependecy-hell

Upvotes: 2

T.J. Crowder
T.J. Crowder

Reputation: 1074335

Overview

You have a couple of choices. You can have your code using those functions look like this, using callbacks:

getStuff(function(results) {
    getMoreStuff(results, doSomethingWithStuff);
});

or like this, using jQuery's Deferred and Promise objects:

getStuff().then(getMoreStuff).then(doSomethingWithStuff):

Using callbacks

Have both getStuff and getMoreStuff accept an argument that is a callback to call when they're done, e.g.:

function getStuff(callback) {
//                ^------------------------------ callback argument
    $.ajax({
        ...
        success: function(results) {
            // other functions involving results
            callback(results);
//          ^------------------------------------ use the callback arg
        }
    });
}

...and similarly for getMoreStuff.

Using Deferred and Promise

jQuery's ajax function integrates with its Deferred and Promise features. You can just add return to your existing functions to make that work, e.g.:

function getStuff(callback) {
    return $.ajax({
        ...
    });
}

(Note: No need for the success callback.)

Then this code:

getStuff().then(getMoreStuff).then(doSomethingWithStuff);

does this:

  1. getStuff starts its ajax call and returns the Promise that call creates.

  2. When that ajax call completes and resolves the promise, getMoreStuff is called with the results of the ajax call as its first argument. It starts its ajax call.

  3. When getMoreStuff's ajax call completes, doSomethingWithStuff is called with the results of that call (the one in getMoreStuff).

It's important to use then, not done, in order to get the correct results passed on at each stage. (If you use done, both getMoreStuff and doSomethingWithStuff will see the results of getStuff's ajax call.)

Here's a full example using ajax:

Fiddle | Alternate Fiddle with the ajax calls taking one second each (makes it easier to see what's happening)

function getStuff() {
    display("getStuff starting ajax")
    return $.ajax({
        url: "/echo/json/",
        type: "POST",
        data: {json: '{"message": "data from first request"}'},
        dataType: "json"
    });
}

function getMoreStuff(results) {
    display("getMoreStuff got " + results.message + ", starting ajax");
    return $.ajax({
        url: "/echo/json/",
        type: "POST",
        data: {json: '{"message": "data from second request"}'},
        dataType: "json"
    });
}

function doSomethingWithStuff(results) {
    display("doSomethingWithStuff got " + results.message);
}

getStuff().then(getMoreStuff).then(doSomethingWithStuff);

function display(msg) {
    var p = document.createElement('p');
    p.innerHTML = String(msg);
    document.body.appendChild(p);
}

Output:

getStuff starting ajax

getMoreStuff got data from first request, starting ajax

doSomethingWithStuff got data from second request

You don't need to be using ajax to get the benefit of this, you can use your own Deferred and Promise objects, which lets you write chains like this:

one().then(two).then(three);

...for any situation where you may have asynchronous completions.

Here's a non-ajax example:

Fiddle

function one() {
    var d = new $.Deferred();
    display("one running");
    setTimeout(function() {
      display("one resolving");
      d.resolve("one");
    }, 1000);
    return d.promise();
}

function two(arg) {
    var d = new $.Deferred();
    display("Two: Got '" + arg + "'");
    setTimeout(function() {
      display("two resolving");
      d.resolve("two");
    }, 500);
    return d.promise();
}

function three(arg) {
    var d = new $.Deferred();
    display("Three: Got '" + arg + "'");
    setTimeout(function() {
      display("three resolving");
      d.resolve("three");
    }, 500);
    return d.promise();
}

one().then(two).then(three);

function display(msg) {
    var p = document.createElement('p');
    p.innerHTML = String(msg);
    document.body.appendChild(p);
}

Output:

one running

one resolving

Two: Got 'one'

two resolving

Three: Got 'two'

three resolving

These two (the ajax example and the non-ajax example) can be combined when necessary. For instance, if we take getStuff from the ajax example and we decide we have to do some processing on the data before we hand it off to getMoreStuff, we'd change it like this: Fiddle

function getStuff() {
    // Create our own Deferred
    var d = new $.Deferred();
    display("getStuff starting ajax")
    $.ajax({
        url: "/echo/json/",
        type: "POST",
        data: {json: '{"message": "data from first request"}', delay: 1},
        dataType: "json",
        success: function(data) {
            // Modify the data
            data.message = "MODIFIED " + data.message;

            // Resolve with the modified data
            d.resolve(data);
        }
    });
    return d;
}

Note that how we use that didn't change:

getStuff().then(getMoreStuff).then(doSomethingWithStuff);

All that changed was within getStuff.

This is one of the great things about the whole "promise" concept (which isn't at all specific to jQuery, but jQuery gives us handy versions to use), it's fantastic for decoupling things.

Upvotes: 11

Arun P Johny
Arun P Johny

Reputation: 388316

Try

function getStuff() {
    return $.ajax({
        ...
        success: function(results) {
            // other functions involving results
        }
    });
}

function getMoreStuff(results) {
    return $.ajax({
        ...
        success: function(moreResults) {
            // other functions involving moreResults
        }
    );
}

Then

getStufff().done(function(){
    getMoreStuff().done(doSomethingWithStuff)
})

etc

Upvotes: 5

iCollect.it Ltd
iCollect.it Ltd

Reputation: 93561

Pass callbacks that accept a parameter:

function getStuff( callback ) {
    $.ajax({
        ...
        success: function(results) {
            // callback with result
            callback(results);
        }
    });
}

function getMoreStuff(results, callback) {
    $.ajax({
        ...
        success: function(moreResults) {
            // callback with result
            callback(moreResults);
        }
    );
}

function doSomethingWithStuff(results, callback) {
    // process results via some means not described herein :)
    if (callback){
        // callback yet again with results, but only if callback provided this time
        callback(stillMoreResults);
    }
}

Then use with something like:

getStuff(function(results) { 
    getMoreStuff(results, function(moreresults){
             doSomethingWithStuff(moreresults);
        });
    };

This pattern is generally useful for any async operations. It is not specific to Ajax calls (I used it to create a complete animated board game in JQuery).

Upvotes: 4

Related Questions