3

I'm building a plugin that fetches info for a bunch of images in JSON, then displays them in some dialog for selection. Unfortunately, my first intuition pretty clearly results in a race condition:

var ImageDialog = function () {};
ImageDialog.prototype.items = [];

ImageDialog.prototype.fetch_images() {
    var parse_images = function(data) {
        // Magically parse these suckers.
        data = awesome_function(data);
        this.items = data;
    };

    magicalxhrclass.xhr.send({"url": 'someurl', "success": parse_images, "success_scope": this});
}

ImageDialog.prototype.render = function () {
    this.fetch_images();
    // XHR may or may not have finished yet...
    this.display_images();
    this.do_other_stuff();
};

var monkey = new ImageDialog();
monkey.render();

Off of the top of my head, I think I could fix this by changing the parse_images callback to include the rest of the render steps. However, that doesn't look quite right. Why would the fetch_images method be calling a bunch of things about displaying images?

So: what should I do here?

I am pretty certain deferreds would help, but alas: I need to write this without any external libraries. :(

Comments on other code smells would be nice, too!

Bacon
  • 211
  • 3
  • 9
  • I think promises would be a good solution for you. Have a look at this library for example: https://github.com/cujojs/when. Wikipedia: https://en.wikipedia.org/wiki/Futures_and_promises. Her is an example with jQuery deferreds, at the end of my answer: http://stackoverflow.com/a/14220323/218196. – Felix Kling Mar 20 '13 at 01:27
  • Felix, I agree that promises and deferreds would be great for this. Unfortunately, as I state in the question, I can't use any libraries for this project. :( thanks for the link though -- I'll check it out! – Bacon Mar 20 '13 at 03:17
  • Oh sorry, I didn't see that part :-/ (I feel silly now). It should not be too hard to implement basic promise/deferred functionality yourself though. Check out the source code of that library. – Felix Kling Mar 20 '13 at 07:26

3 Answers3

2

How about this?

var ImageDialog = function () {
    this.items = []; // just in case you need it before the images are fetched
};

ImageDialog.prototype.fetch_images(callback) {
    var that = this;
    function parse_images (data) {
        // Magically parse these suckers.
        data = awesome_function(data);
        that.items = data;
        callback.apply(that);
    };

    magicalxhrclass.xhr.send({"url": 'someurl', "success": parse_images, "success_scope": this});
}

ImageDialog.prototype.render = function () {
    this.fetch_images(function(){
        this.display_images();
        this.do_other_stuff();
    });
};

var monkey = new ImageDialog();
monkey.render();
Dagg Nabbit
  • 75,346
  • 19
  • 113
  • 141
  • `var foo = function` also smelled ;) – Dagg Nabbit Mar 20 '13 at 01:41
  • 1
    One more thing that smells: that.items = data will shadow the prototype's items property. That's actually good, or all instances would share the same data. Conclusion: get rid of ImageDialog.prototype.items = []; – bfavaretto Mar 20 '13 at 02:33
2

In general, the basic idea you can use is that when a regular program would use a return statement (meaning, "My function is done now do your job!") an asynchronous continuation-passing program would instead use a ballcabk function that gets explicitly called

function fetch_images(callback){
   magicalXHR({
       success: function(data){
           parse_images(data);
           callback(whatever);
       }
   }
}

or, if parse_images is itself an async function:

parse_images(data, callback)

Now when you call fetch_images the code after it goes into a callback instead of assuming that fetch_images will be done when it returns

fetch_images(function(
   display_images()
})

By using callbacks you can emulate pretty well what a traditional program could do (in fact its a fairly mechanical translation between one form of the other). The only problem you will now encounter is that error handling gets tricky, language features like loops don't play well with async callbacks and calbacks tend to nest into callback hell. If the callbacks start getting too complex, I would investigate using one of those Javascript dialects that compiles down to continuation-passing-style Javascrit (some of them work without needing extra libraries at runtime).

hugomg
  • 68,213
  • 24
  • 160
  • 246
  • Very well-said. I know that I _could_ use CPS like this, as GGG also suggested. Unfortunately, something just rubs me the wrong way about this in a very OOP context. What in the world does `display_images` have to do with `fetch_images` such that it should be a parameter? Mightn't it be better named `fetch_images_and_continue_with` at that point? (I think I may just be trying too hard to make this seem like non-async programming.) – Bacon Mar 20 '13 at 16:34
  • Okay, fine. I think I need to just stop worrying and love the CPS. Thanks for your help! – Bacon Mar 20 '13 at 17:37
  • @Bacon: Its OK to hate the callbacks. But you need to get used to them :) – hugomg Mar 20 '13 at 21:47
0

Here's a thought about what to do.

ImageDialog.prototype.fetch_images() {
    var parse_images = function(data) {
        // Magically parse these suckers.
        data = awesome_function(data);
        this.items = data;
        fetch_images.caller() // Unfortunately, this is nonstandard/not in the spec. :(
    };

    magicalxhrclass.xhr.send({"url": 'someurl', "success": parse_images, "success_scope": this});
}

ImageDialog.prototype.render = function () {
    if (this.items === []) {
        this.fetch_images()
        return;
    } else {
        this.display_images();
        this.do_other_stuff();
    };
};

This way I'm not passing some implementation detail to fetch_images, and I get caching, to boot. Am I still trying too hard to escape CPS, or is this sensible?

Bacon
  • 211
  • 3
  • 9