Wednesday, 24 October 2012

Asynchronicity

I see questions like this one a fair bit: The author has written this code (and is apparently using jQuery):

function Obj() {
    this.id = 0;
    this.name = '';
}

Obj.prototype.setName = function(name) {
    this.name = name;
};

function init() {
    var object1;

    object1 = new Obj();
    object1.setName("Chris");
    alert(object1.name); // alerts 'Chris'

    $.post('my_json_list.php', function(data) {
        object1.setName(data.name);
        alert(object1.name); // alerts 'John'
    });

    // After completing the data transfer alert the value of object
    alert('After data transfer the value is: '+ object1.name); // alerts 'Chris' (!)
}

The comment at the end embodies the question: The author expects the ajax call to be complete at that point. But it isn't. That code runs after the request has been started, but before it completes. So of course object1.name is still "Chris" — nothing has changed it yet. The author of that code is not remotely alone in this confusion, I've answered at least a dozen questions on StackOverflow (such as this one) where this was the fundamental problem. So:

Shout it from the rooftops: Ajax calls are asynchronous. :-) (By default.)

That means the code that starts the call runs, then any code following it, and then the ajax callbacks happen some time later. E.g., here's that init code again with some comments saying when it happens:

function init() {
    // 1: First this code runs...
    var object1;

    object1 = new Obj();
    object1.setName("Chris");
    alert(object1.name); // alerts 'Chris'

    // 2: ...and now this code *starts* the request...
    $.post('my_json_list.php', function(data) {
        // 4: (See #3 below) Then, some time AFTER #3 below, this code runs
        object1.setName(data.name);
        alert(object1.name); // alerts 'John'
    });

    // 3: ...now the request has started but is not yet finished,
    // so `object1.name` remains `"Chris"`
    alert('After data transfer the value is: '+ object1.name); // alerts 'Chris' (!)
}

So what to do about it? Easy, don't use the result of the call at point #3 above, use it at point #4 above.

This simple fact has an important implication: You cannot use the result of an ajax call as the return value of a function. This code is fundamentally flawed:

function getSomething(id) {
    var returnValue;

    $.ajax({
        url:     "/path/to/resource",
        data:    {id: id},
        success: function(result) {
            returnValue = result.data;
        }
    });

    return returnValue; // WRONG
}

That's written so you can call it like this:

something = getSomething("some ID");
if (something) {
    doSomethingWith(something);
}
else {
    doSomethingElse();
}

The problem is, it doesn't matter what id value you feed into doSomething, I can tell you what its return value will be every single time: undefined. The call to doSomething will complete, and return the default value of returnValue (which is undefined), and then some time later the callback will occur and set the value of returnValue (but it's a bit like a tree falling in the forest, as at that point no one uses it).

Can doSomething be fixed? Yes, in two ways: We can force the ajax call to be synchronous (bad idea) or we can embrace the event-driven nature of web applications (good idea).

Let's look at that first option: We can (for now) add the option async: false to the ajax call (that option is deprecated — but functional — in jQuery 1.8 and will go away at some point, though not apparently in 1.9). If we do that, the call to ajax doesn't return until the ajax call completes and its callbacks have finished. This is a bad idea because ajax calls can take significant time (even a tenth of a second is significant to human perception), and JavaScript on browsers is single-threaded. So during the time the ajax call takes, our page becomes non-responsive. (In fact, on several browsers, the entire browser — including unrelated tabs — just seems to lock up.)

Okay, so we don't want that. How do we fix it so doSomething can return what we want it to? By making doSomething return its result using a callback, just like ajax does:

function getSomething(id, callback) {
    $.ajax({
        url:     "/path/to/resource",
        data:    {id: id},
        success: function(result) {
            callback(result.data);
        }
    });
}

...and calling it like this...

getSomething("some ID", function(something) {
    if (something) {
        doSomethingWith(something);
    }
    else {
        doSomethingElse();
    }
});

Voilà, we've embraced the event-driven nature of web applications, kept our pages responsive, and it didn't even have that much impact on the calling code.

Happy (async) coding!

1 comment:

Ben said...

Very nice. And helpful!