Distigme

A little learning is a dangerous thing.

Returning a value from a function with AMD

1 Comment

These days I’m still getting used to AMD in Dojo. It’s a shift in thinking, that whenever you want to use a module, all you can really do is schedule something to be executed when the module finally gets loaded, quite possibly millions of nanoseconds in the future.

This pattern fundamentally breaks the request-response paradigm inherent in calling a function and getting its return value. Let’s say you have some old code like this (pretend it’s not trivial):

function getLogoTitle() {
    var logoNode = dojo.byId("logo");
    return logoNode.title;
}
function displayWelcome() {
    alert(getLogoTitle());
}

Now, you go to convert this to modern AMD style, and a first pass looks like:

function getLogoTitle() {
    require(["dojo/dom"], function(dom) {
        var logoNode = dom.byId("logo");
        return logoNode.title;
    });
}
function displayWelcome() {
    alert(getLogoTitle());
}

This will not work, and it may not be obvious why. The return is now returning from the inner anonymous function rather than getLogoTitle(), and the latter now returns nothing. If you move the return outside the require, it doesn’t have access to logoNode, and if you move that declaration outside the require, like this:

function getLogoTitle() {
    var logoNode;
    require(["dojo/dom"], function(dom) {
        logoNode = dom.byId("logo");
    });
    return logoNode.title;
}
function displayWelcome() {
    alert(getLogoTitle());
}

You can still get nothing, since the assignment happens in the future. This whole approach is fundamentally flawed, since you are trying to use at present a value that will not exist until the distant future.

The way to fix that is to schedule what you want to do with the value once it is obtained. But that stuff you want to do is actually back in the calling function displayWelcome, so you need to coordinate with the caller via a callback.

The simplest solution is to restructure like this:

function getLogoTitle(callback) {
    require(["dojo/dom"], function(dom) {
        var logoNode = dom.byId("logo");
        callback(logoNode.title);
    });
}
function displayWelcome() {
    getLogoTitle(function(title) {
        alert(title);
    });
}

As your needs grow more complex, you will appreciate the more full-featured promise-based solution offered by Deferred. The tricky part can be accessing the Deferred module itself. One crude way is to take it as a parameter:

function getLogoTitle(Deferred) {
    var result = new Deferred();
    require(["dojo/dom"], function(dom) {
        var logoNode = dom.byId("logo");
        result.resolve(logoNode.title);
    });
    return result;
}
function displayWelcome() {
    require(["dojo/Deferred", "dojo/when"], function(Deferred, when) {
        when(getLogoTitle(Deferred), function(title) {
            alert(title);
        });
    });
}

This when style accommodates both functions that return values directly and functions that return promises. So, rather than rely on knowledge of which type of function you are calling, a good habit is to rely on when for handling return values.

Now, passing in the Deferred module is ugly. There are ways to load it synchronously in the global scope, but the preferred solution is to wrap a function like getLogoTitle in a module. If both of the function and its caller are already in a module together, you’re all set. If the caller is somewhere else, though, the idea is to expand its require to include the module, so that by the time the callback executes, Deferred and any other modules needed immediately by getLogoTitle have already been loaded and hooked up and can be used directly. But writing a module involves breaking code out into another file, so the time to do that is when you your code is complex enough to benefit from separate modules.

There is a shortcut, though, which is to make a simple promise without using Deferred. Basically, you return an object with a then() function:

function getLogoTitle() {
    return {
        then: function(callback) {
            require(["dojo/dom"], function(dom) {
                var logoNode = dom.byId("logo");
                callback(logoNode.title);
            });
        }
    };
}
function displayWelcome() {
    require(["dojo/when"], function(when) {
        when(getLogoTitle(), function(title) {
            alert(title);
        });
    });
}

The flow goes like this:

  1. Let’s start with displayWelcome() having loaded the when module.
  2. getLogoTitle() is called.
  3. getLogoTitle() immediately returns a promise object containing an embedded then function.
  4. when is called with this promise and with another anonymous callback function.
  5. when sees that its first parameter has a then function and calls it, passing the callback as a parameter.
  6. The then function begins executing.
  7. The require starts off an asynchronous module load. But is the module already loaded?
    • If already loaded, proceed directly to the callback in the next step.
    • If not, register the callback and return immediately, unwinding the stack. If, later on, the module loading ever finishes, then the loader will call the callback in the next step.
  8. The require callback, which is the anonymous function(dom), executes, using the now-loaded dom do to its business.
  9. Having done its business, it passes its result to yet another callback, the anonymous function(title).
  10. At last, this callback has the return value from the original code, and it proceeds to display the alert.

It seems a little convoluted, with functions flying everywhere, but if you can get your head around it, you will have a solid grasp of asynchronous JavaScript.

Advertisements

One thought on “Returning a value from a function with AMD

  1. thanks for these tips! the “lightweight promise” (object with a then property) was quite useful

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s