Reputation: 1703
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.
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']
}
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));
}
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
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.
<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>
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();
}
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();
});
}
};
});
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