Specifically, jQuery calls define() in the middle of the file, before
the execution of the body of jQuery is done. In curl, it seems to call
code that has already loaded that depends on jQuery immediately when
receiving the define() call for jQuery. This can cause a problem if
that dependent code immediately uses something in jQuery that is
defined after the define call.
In requirejs, it waits for the current script to finish executing
before seeing if dependencies can be satisfied.
Should we try to standardize this execution behavior one way or another?
I like the behavior requirejs uses because it is the behavior that
must occur for anonymous modules in the browser (the name is not known
until the script finishes executing, at least in non-IE browsers), and
it also maps better to the built environment, where jquery would be
first in the built file, execute completely before the next module is
run.
I also believe it gives script authors more flexibility in adopting
optional calls to define() -- the patches to existing code can be
smaller for code with no dependencies, particularly if they also do a
commonjs/node call setup, where they may want to use exports to hang
their properties. In that case, the adapter code can happen at the top
of the file.
Example:
(function () {
var myLib = {};
if (typeof exports === 'object') {
myLib = exports;
} else if (typeof define === 'function' && define.amd) {
define(myLib);
} else {
//window globals.
this.myLib = myLib;
}
//Set up myLib here.
}.call(this));
While it is possible to move that define() call to the bottom of the
file, I believe most people would prefer to have all the module
adapter logic together. At least I would.
But it would be good to get other implementers to comment though, and
for John to expand on the execution model in curl. Rawld, what does
dojo do?
James
It is a bit more subtle than that: it is when modules that depend on
the output of the factory function are called.
That means the factory function has been called, but in the case of
jQuery, if the factory function was called right away, it would not
necessarily be bad, but giving the result of the factory function to
other factories that depended on jQuery could result in an error since
the file containing jQuery is still executing -- not all of the
properties have been attached yet to the jQuery "export".
So, imagine a module called "a" that depends on jquery and calls
jQuery.extend(), and it has loaded before jQuery loads. It looks
something like:
define('a', ['jquery'], function ($) {
//part of setting up a is to use jQuery
$.extend();
});
And jQuery loads, and its content looks roughly like this:
(function () {
function jQuery() {};
define('jquery', [], function () {
return jQuery;
});
jQuery.extend = function () {};
}());
So if the loader gives jQuery to 'a' as part of jQuery's call to
define(), instead of waiting for the file containing jQuery finishes
executing, there will be an error.
James
Right, I was not framing this as a need to change requirejs, but more
for module users and authors, what behavior should they expect. I
mentioned how requirejs does it because it is one way, I'm not sure
what others do, and it is a consistent behavior that gives a bit more
flexibility for module authors.
The performance arguments do not resonate with me (in particular the
sync server case -- node does a few IO operations w/try/catch to load
modules, and the perf is fine for them). Builds will be more effective
overall with achieving performance, and for me consistency is a more
important design goal.
But now we're just talking about perceptions and performance, we would
need more real data.
So let's keep the arguments focused on the tension between these two
items, which are really the core of my concern:
1) What would an AMD implementer expect the behavior of a define call to be?
2) What is easy for existing library authors to implement?
1) In a "modules only" world, I believe it is reasonable to assume
that once define() has returned, the module is defined.
2) However, for existing libraries, that approach limits the options
for boilerplate. I was concerned about libraries like underscore or
backbone, whose authors are not necessarily warm on the idea of
supporting AMD.
However, I think there are workable solutions for those cases. It will
require another change to underscore to fit in with this behavior, and
I'm not too excited about pushing for that, but the change is fairly
straightforward, just move the module detection/registration block to
the end of the file since underscore wants to export a function, so it
defines it up front anyway -- it does not want to rely on a exports
value to start.
So, I'm OK with just stating that when define() returns, the module
should be considered completely constructed at that point. I can add
some wording to this effect in the AMD API page too. I want to
illustrate that API page with a few more examples too. I'll post back
to the list when I get to it, which may not be for a little while.
On a side note about umd boilerplate:
> From the dozen or so people I've chatted with about universal module
> formats (I know, a dozen is not necessarily a good sampling), it's
> preferable to define your module at the top of the file and put the
> boilerplate at the bottom.
Agreed, I like it at the bottom, but for items with dependencies, I
think it may work out better with the boilerplate at the top, much to
my disappointment. Compare this patch I was trying to do with backbone
that does the work mostly at the bottom:
https://github.com/jrburke/backbone/compare/documentcloud:master...jrburke:optamd
vs doing it at the top:
https://github.com/jrburke/backbone/compare/documentcloud:master...jrburke:optamd3
Which is a variation inspired from Kris Kowal's q library:
https://github.com/kriskowal/q/blob/master/q.js#L13
With the boilerplate at the bottom, it really requires another level
of indentation, and it seems a bit more wordy. I'm not sure if either
approach will work for Backbone yet, but doing it at the top seems
like a less invasive change for an existing library than doing the
work at the bottom.
James
Important: Despite being called "AMD", the node module loader is in fact synchronous, and using define()
does not change this fact. Node executes the callback immediately, so please plan your programs accordingly.
In a sync environment, the module would still run to completion before
continuing with the execution of the module that required that
dependency, the define() call was just used to register the exports.
Otherwise the call sequencing gets impossible to track.
That said, I still think you make a good point that if you just look
at define() code, anyone looking at that and trying to implement a
loader would expect "once the factory function is completed the module
is defined".
Note that this is only an issue with code that does not have any
dependencies. Once there is a dependency, this sort of gray area goes
away, the module will likely need to be completely wrapped in a define
call execution to work.
I am concerned that we make it easy for these base libraries to opt in
to calling AMD. I think that is a valid concern, but giving those
cases a *slight bit* more wiggle room in a way that muddies
understanding if/when AMD becomes standardized does not seem to be
worth the tradeoff now for me.
James
I did not quite follow this -- I do not know what those numbers
reference. Maybe you could clarify. It sounds a little bit like
CommonJS stuff.
If so, the issue in this discussion is a bit more nuanced. While the
"wait to execute factory until first require call" behavior you cite
would avoid this particular issue as a side effect, that behavior
makes the APIs more verbose, and AMD works fine without doing so.
The more direct question to ask is "should a module be considered
defined once the factory function has finished?". I think John makes a
good case that the answer should be "yes". It makes sense. It is just
a bit tricky since we are dealing with bootstrapping existing web
libraries into modular behavior, and the opt-in AMD calls brought up
the issue.
James