Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 568
Jay Avatar asked Aug 09 '13 10:08

Jay


1 Answers

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.

like image 63
T.J. Crowder Avatar answered Oct 16 '22 18:10

T.J. Crowder