AweSIM
AweSIM

Reputation: 1703

Marionette + RequireJs - Issues with lazy loading

I've been trying to practice dependency resolution for Mariotte modules using Require.js. Lately, I've tried to learn how to source files lazily, ie. loading relevant .js files only when needed and not at application startup. However this is proving to be a nightmare. The following code fragments is how I'm trying to do things right now.

define(['app'], function(App) {
    App.module('Header', function(Header, App, Backbone, Marionette, $, _) {

        this.doSomething = function() { alert('Did something!'); }
        this.getSomething = function() { return 'Something'; }

    }
});

Suppose that doSomething needs subdep to be loaded before it can do its work. I can ensure that as follows: I can verify in ChromeDevtools that subdep is only loaded when this.doSomething() is called.

this.doSomething = function() {
    require(['subdep'], _.bind(function() {
        alert(this.SubDep.showSomething());
    }, this));
}

From here on, I have a couple of issues / questions that I'm looking forward to have addressed.

  1. I need to use _.bind() to retain the value of this. The require(...) also pollutes the code visually. Is there a way around that, by maybe customizing the Marionette.Module.prototype? Something like this would be ideal:

    this.doSomething = function() { alert(this.SubDep.showSomething()); }
    myRequireHash: {
        'this.doSomething' : ['subdep'],
        'this.getSomething' : ['subdep', 'underscore']
    }
    
  2. Suppose this.getSomething needs to return a value to the caller. The following does not work, obviously, since the require statement initiates asynchronous loading and returns immediately. How do I work around this? I need to load the dependency when actually needed and also be able to return a value.

    this.getSomething = function() {
        require(['subapp'], _.bind(function() {
            return this.SubApp.getSomething();
        }, this));
    }
    
  3. As an extension to point #2, suppose the caller needs to call this.doSomething() AFTER calling this.getSomething(). Since the require calls are asynchronous, can I somehow return a promise from this.getSomething() that could be used to ensure that the two functions will be called in sequence? If so, then how?

Asim


Update

Using Paul's ideas, here is how I've tackled my situation:

function RequirePromise (pListDeps, oContext, fnFunc)
{
    return $.Deferred(function(oDef)
    {
        require(pListDeps, function()
        {
            var pArgs = [];
            if (fnFunc)
            {
                if (oContext) fnFunc = _.bind(fnFunc, oContext);
                pArgs.push(fnFunc.apply(fnFunc, arguments));
            }
            oDef.resolveWith(oContext, _.union(pArgs, arguments));
        });
    }).promise();
}

pListDeps is a list of dependencies to be loaded. oContext is the preferred default context for the promise functions to be in. fnFunc is an optional function that runs in the given function (without chaining to the then function). The return value from the function is available as the first parameter in then/done. The loaded dependencies are available as 2nd parameter onwards. I can use this as any of the following:

RequirePromise(['app'], this).done(function(App) { console.log(arguments); }

RequirePromise(['app'], this, function(App) { console.log(App); return true; })
    .done(function(oRet, App) { console.log(arguments); }

RequirePromise(['app'], this)
    .then(function() { return RequirePromise(['subapp']); })
    .done(function() { console.log('Both app and subapp loaded!'); }

Thanks Paul =)

Upvotes: 1

Views: 721

Answers (1)

Paul Grime
Paul Grime

Reputation: 15104

You can't really do what you want without either providing callbacks or deferreds/promises for all the async methods.

JavaScript doesn't really have a wait/waitUntil/waitFor concept like other languages, where you can wait for an async task to finish. And indeed you mostly don't want to do this, as it would 'hang' the browser.

This jsfiddle is an example of what I mean.

The JS below assumes some knowledge of jQuery Deferreds.

The HTML:

<script>
require = {
    paths: {
        "jquery": "http://code.jquery.com/jquery-2.0.3",
        "mymodule": "https://rawgithub.com/gitgrimbo/5689953/raw/9b44d7e5f504b2245331be3ed3fcbb7bf8635da6/gistfile1"
    }
};
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.8/require.min.js"></script>
<button>click me</button>

JS (1):

dfdRequire is a function that calls require() but returns a jQuery promise object.

The callback for require() is an internal one, and the dependencies returned by require() are used to resolve the deferred.

Basically this converts from a callback-based system to a promise-based one.

function dfdRequire(deps) {
    return $.Deferred(function(dfd) {
        require(deps, function() {
            dfd.resolve.apply(dfd, arguments);
        });
    }).promise();
}

This could actually be reduced to:

function dfdRequire(deps) {
    return $.Deferred(function(dfd) {
        require(deps, dfd.resolve.bind(dfd));
    }).promise();
}

JS (2):

Create the dummy app module that uses the above function.

The getSomething() method will use dfdRequire() to load the "mymodule" module, and then use the then() method to actually use the module.

The function passed to then() uses the mymodule value by uppercasing it, and returning this new value. This means that when the method returns, it actually will return the uppercased value.

define("app", ["jquery"], function($) {
    return {
        doSomething: function(value) {
            console.log("doSomething with " + value);
        },
        getSomething: function() {
            // Load the dependency as a promise, and return that promise to the caller,
            // so the caller can also await its resolution.
            return dfdRequire(["mymodule"]).then(function(mymodule) {
                console.log("Loaded mymodule. Value=" + mymodule);
                // return the module value as-is,
                // or optionally perform some transformation.
                return mymodule.toUpperCase();
            });
        }
    };
});

JS (3):

Pull in the app and use its methods.

app.getSomething() will return a promise. We use this promise in a chain (to demonstrate that promise calls can be chained). Firstly, the value is passed to console.log() which will print the value. Then we call app.doSomething().

require(["jquery", "app"], function($, app) {
    console.log($.fn.jquery);
    $("button").first().click(function(evt) {
        console.log(evt);
        app.getSomething()
            .done(console.log.bind(console))
            .done(app.doSomething.bind(app));
    });
});

We use Function.bind() as shorthand for what could also be written as.

        app.getSomething()
            .done(function(value) {
                console.log(value);
            })
            .done(function(value) {
                app.doSomething(value);
            });

Results in:

2.0.3
Object { originalEvent=Event click, type="click", timeStamp=88448339, more...}
Loaded mymodule. Value=my-library
MY-LIBRARY
doSomething with MY-LIBRARY

Upvotes: 3

Related Questions