Skip to content

Commit

Permalink
Landing the initial jQuery.require() work. Need to add in remote scri…
Browse files Browse the repository at this point in the history
…pt queueing.
  • Loading branch information
jeresig committed Dec 10, 2009
1 parent 88572ee commit a5b2940
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 56 deletions.
43 changes: 9 additions & 34 deletions src/ajax.js
Expand Up @@ -5,7 +5,6 @@ var jsc = now(),
jsre = /=\?(&|$)/,
rquery = /\?/,
rts = /(\?|&)_=.*?(&|$)/,
rurl = /^(\w+:)?\/\/([^\/?#]+)/,
r20 = /%20/g;

jQuery.fn.extend({
Expand Down Expand Up @@ -273,43 +272,19 @@ jQuery.extend({
}

// Matches an absolute URL, and saves the domain
var parts = rurl.exec( s.url ),
remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
var remote = jQuery.isRemote( s.url );

// If we're requesting a remote document
// and trying to load JSON or Script with a GET
if ( s.dataType === "script" && type === "GET" && remote ) {
var head = document.getElementsByTagName("head")[0] || document.documentElement;
var script = document.createElement("script");
script.src = s.url;
if ( s.scriptCharset ) {
script.charset = s.scriptCharset;
}

// Handle Script loading
if ( !jsonp ) {
var done = false;

// Attach handlers for all browsers
script.onload = script.onreadystatechange = function(){
if ( !done && (!this.readyState ||
this.readyState === "loaded" || this.readyState === "complete") ) {
done = true;
success();
complete();

// Handle memory leak in IE
script.onload = script.onreadystatechange = null;
if ( head && script.parentNode ) {
head.removeChild( script );
}
}
};
}

// Use insertBefore instead of appendChild to circumvent an IE6 bug.
// This arises when a base node is used (#2709 and #4378).
head.insertBefore( script, head.firstChild );
jQuery.require({
url: s.url,
scriptCharset: s.scriptCharset,
success: function() {
success();
complete();
}
});

// We handle everything using the script element injection
return undefined;
Expand Down
183 changes: 161 additions & 22 deletions src/core.js
Expand Up @@ -32,6 +32,9 @@ var jQuery = function( selector, context ) {
// Match a standalone tag
rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/,

// Match portions of a URL
rurl = /^(\w+:)?\/\/([^\/?#]+)/,

// Keep a UserAgent string for use with jQuery.browser
userAgent = navigator.userAgent.toLowerCase(),

Expand All @@ -40,7 +43,10 @@ var jQuery = function( selector, context ) {

// The functions to execute on DOM ready
readyList = [],


// The queue of required scripts currently being loaded
requireQueue = [],

// Save a reference to some core methods
toString = Object.prototype.toString,
hasOwnProperty = Object.prototype.hasOwnProperty,
Expand Down Expand Up @@ -339,22 +345,7 @@ jQuery.extend({
// Remember that the DOM is ready
jQuery.isReady = true;

// If there are functions bound, to execute
if ( readyList ) {
// Execute all of them
var fn, i = 0;
while ( (fn = readyList[ i++ ]) ) {
fn.call( document, jQuery );
}

// Reset the list of functions
readyList = null;
}

// Trigger any bound ready events
if ( jQuery.fn.triggerHandler ) {
jQuery( document ).triggerHandler( "ready" );
}
readyReady();
}
},

Expand Down Expand Up @@ -425,6 +416,93 @@ jQuery.extend({
}
}
},

require: function( options, callback ) {
var xhr, requestDone, ival, head, script;

if ( options && !options.url ) {
options = { url: options, success: callback };
}

if ( !options || jQuery.requireCache[ options.url ] != null ) {
return;
}

requireQueue.push( options );
jQuery.requireCache[ options.url ] = false;

// If the DOM ready event has already occurred, we need to go synchronous
if ( !jQuery.isRemote( options.url ) ) {
xhr = window.ActiveXObject ?
new ActiveXObject("Microsoft.XMLHTTP") :
new XMLHttpRequest(),

xhr.open( "GET", options.url, !jQuery.isReady );
xhr.send( null );

function checkDone() {
if ( !requestDone && xhr && xhr.readyState === 4 ) {
requestDone = true;

// clear poll interval
if ( ival ) {
clearInterval( ival );
ival = null;
}

execRequire( options.url, xhr.responseText );
}
}

if ( jQuery.isReady ) {
checkDone();
} else {
ival = setInterval( checkDone, 13 );
}

// Otherwise we can still load scripts asynchronously
} else {
head = document.getElementsByTagName("head")[0] || document.documentElement;
script = document.createElement("script");

script.src = options.url;

if ( options.scriptCharset ) {
script.charset = options.scriptCharset;
}

// Attach handlers for all browsers
script.onload = script.onreadystatechange = function() {
if ( !requestDone && (!this.readyState ||
this.readyState === "loaded" || this.readyState === "complete") ) {

requestDone = true;

// Handle memory leak in IE
script.onload = script.onreadystatechange = null;

if ( head && script.parentNode ) {
head.removeChild( script );
}

execRequire( options.url );
}
};

// Use insertBefore instead of appendChild to circumvent an IE6 bug.
// This arises when a base node is used (#2709 and #4378).
head.insertBefore( script, head.firstChild );
}
},

// Keep track of URLs that have been loaded
requireCache: {},

// Check to see if a URL is a remote URL
isRemote: function( url ) {
var parts = rurl.exec( url );
return parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
},

// See test/unit/core.js for details concerning isFunction.
// Since version 1.3, DOM methods and functions like alert
Expand Down Expand Up @@ -638,13 +716,74 @@ if ( indexOf ) {
// All jQuery objects should point back to these
rootjQuery = jQuery(document);

function readyReady() {
if ( jQuery.isReady && requireQueue.length === 0 ) {
// If there are functions bound, to execute
if ( readyList ) {
// Execute all of them
var fn, i = 0;
while ( (fn = readyList[ i++ ]) ) {
fn.call( document, jQuery );
}

// Reset the list of functions
readyList = null;

// Trigger any bound ready events
if ( jQuery.fn.triggerHandler ) {
jQuery( document ).triggerHandler( "ready" );
}
}
}
}

function execRequire( url, script ) {
var item, i, exec = true;

jQuery.requireCache[ url ] = true;

for ( i = 0; i < requireQueue.length; i++ ) {
item = requireQueue[i];

if ( item.url === url ) {
if ( script != null ) {
item.script = script;
} else {
next();
continue;
}

}

if ( exec && item.script ) {
jQuery.globalEval( item.script );
next();
} else {
exec = false;
}
}

// Check to see if all scripts have been loaded
for ( var script in jQuery.requireCache ) {
if ( jQuery.requireCache[ script ] === false ) {
return;
}
}

readyReady();

function next() {
if ( jQuery.isFunction( item.callback ) ) {
item.callback();
}

requireQueue.splice( i--, 1 );
}
}

function evalScript( i, elem ) {
if ( elem.src ) {
jQuery.ajax({
url: elem.src,
async: false,
dataType: "script"
});
jQuery.require( elem.src );
} else {
jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
}
Expand Down
9 changes: 9 additions & 0 deletions test/data/require.php
@@ -0,0 +1,9 @@
<?php
error_reporting(0);
$wait = $_REQUEST['wait'];
if($wait) {
sleep($wait);
}

echo "requireTest(" . $_REQUEST['response'] . ");";
?>
67 changes: 67 additions & 0 deletions test/unit/core.js
Expand Up @@ -759,3 +759,70 @@ test("jQuery.isEmptyObject", function(){
// What about this ?
// equals(true, jQuery.isEmptyObject(null), "isEmptyObject on null" );
});

asyncTest(".require() - Document Not Ready, Local", 3, function() {
jQuery.isReady = false;
jQuery.requireCache = {};

var order = [];

window.requireTest = function( num ) {
order.push( num );
equals( order.length, num, "Make sure that the results are coming in in the right order." );

if ( num === 3 ) {
jQuery.isReady = true;
start();
}
};

jQuery.require("data/require.php?wait=1&response=1");
jQuery.require("data/require.php?response=2");
jQuery.require("data/require.php?response=3");
});

asyncTest(".require() - Document Ready, Local", 3, function() {
jQuery.isReady = true;
jQuery.requireCache = {};

var order = [];

window.requireTest = function( num ) {
order.push( num );
equals( order.length, num, "Make sure that the results are coming in in the right order." );

if ( num === 3 ) {
start();
}
};

jQuery.require("data/require.php?wait=1&response=1");
jQuery.require("data/require.php?response=2");
jQuery.require("data/require.php?response=3");
});

asyncTest(".require() - Document Ready, Remote", 3, function() {
jQuery.isReady = true;
jQuery.requireCache = {};

var order = [], old = jQuery.isRemote;

window.requireTest = function( num ) {
order.push( num );
equals( order.length, num, "Make sure that the results are coming in in the right order." );

jQuery.isRemote = old;

if ( num === 3 ) {
start();
}
};

jQuery.isRemote = function(){
return true;
};

jQuery.require("data/require.php?wait=1&response=1");
jQuery.require("data/require.php?response=2");
jQuery.require("data/require.php?response=3");
});

41 comments on commit a5b2940

@jeresig
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In short, jQuery.require:

  • Allows you to request a script asynchronously but still have it delay the execution of the ready event. (This means no page blocking and parallel script downloads!)
  • Additionally all requested scripts are guaranteed to run in the correct order (even though they may be loaded in a different order).
  • These even works for both local files and remote files (although only local files can download scripts in parallel).
  • If jQuery.require is called after DOM ready it becomes synchronous to ensure correction execution order.
  • Supports specifying a callback (in which case it'll always be async)
  • The same script is prevented from loading multiple times.

On the slate: Adding in some form of filters or namespaces, making sure the dynamic scripts are loaded correctly, and adding in the ability to load multiple scripts in a single statement.

@darwin
Copy link

@darwin darwin commented on a5b2940 Dec 11, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@darkhelmet
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a Wayne's World salute is in order!!

SCHWING!!!

@rodbegbie
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love you and I want to have your babies.

@iamnoah
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeresig For the remote script queuing/ordering, I've done almost the same thing here:

http://github.com/iamnoah/writeCapture/blob/master/writeCapture.js (Checkout the Q class)

Feel free to steal concepts or code.

@JonGretar
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crap thats hot.

@jacobandresen
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drool

@hober
Copy link

@hober hober commented on a5b2940 Dec 11, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very awesome.

@FGRibreau
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm loving it !

@dylans
Copy link

@dylans dylans commented on a5b2940 Dec 11, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't wait to see jQuery.require("dojo.data"); or something like that. ;)

@jeresig
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iamnoah: I think you misunderstand what was happening - the script execution is already being queued, the script loading does not need to be queued (and can be heavily parallelized). $.require() is just enforcing that scripts are executed in the right order (which is what I was referring to in my comment, with regards to remote script loading).

@iamnoah
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeresig So are you somehow loading a remote script but prevent it from executing? Or is require blocking the local script execution when a remote script is loading?

BTW, Sorry for the triple comment, spam filter was blocking me without showing an error message...

@aheckmann
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, I guess I should hold off continuing my own work on this type of feature for jQuery. We're just starting to test it out on our current projects at work. It's really great you're building this into jQuery's core.

@jeresig, My vision for this is that it should be the beginning of something bigger - organizing everything on plugins.jquery.com and jqueryui.com into custom configurable on-the-fly downloads of any pluging ever submitted. Would require a CDN to back it though. It works by declaring specific modules that specify which files are required and what css and js dependencies it has, along with any imgs to preload. My early version of this is configured by default to work with all of jQuery-ui like so: jQuery.use('jquery-ui-tabs', callback) or jQuery.use(['jquery-ui-*', 'another-module'], callback). There would also be a server side component optionally available (like YUIs combo service) to bundle all CSS and JS requests into one request each. Themes are configurable as well... Is this a direction you'd be interested in going?

@aheckmann
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeresig Are you saying require() will block DOMReady? I don't think that's a good idea. In my experience, DOMReady is used to progressively enhance the page before it gets painted to the screen. Will this cause the flash of unstyled content problem?

@padolsey
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aheckman, if anything, the usage of DOMReady is one of the causes of FOUC. The only way to affect elements at the precise moment they load is via CSS (e.g. adding a dynamically created <style> element to the <head> before the <body> loads. DOMReady is only slightly better than window.onload... neither prevent FOUC...

@jaubourg
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to agree with aheckmann here. The concept of blocking the jQuery onload event seems unwise to me and the design seems like a poor attempt at a pluggable architecture.

For starter, everything javascript doesn't revolve around the DOM. jQuery itself is a prime example of that. You can do a lot of work with data structures before the DOM is ready. A require system implemented as it is here will bring slower loading pages since it makes it impossible to use this window of opportunity (where javascript can run concurrently with the rendering engine).

Second, if you organize your pages in modules, you'll end up having all of them in cache, which means all $.require being called before the ready event is fired. Say you have a module that makes use of the yahoo geo/map thingy : you create your js module in which you require yahoo's scripts. Second time you load the page, the module itself is in cache, but the yahoo scripts will never be. So you end up having the $.require on the yahoo scripts being called before the ready event and, thus, the page stalls, waiting for those damn scripts while other modules may have all of their scripts already available.

Third, if any module has a $.require on a remote script that never completes, the ready event is NEVER fired and there is no fallback.

Fourth, ajax-related code gets promoted into the core and, as a prime example of why it is a very bad idea, it cannot use $.ajaxSettings so there is no way a user can override the xhr implementation or have any kind of control over how $.require makes its requests. The shuffling of code is daunting to say the least.

From what I gather, the main gain of $.require over $.getScript is the possibility of chaining script loading in a specific order which does require (no pun intended) cumbersome barrier synchronization code as of today.

Anyway, I see nothing here that couldn't be done with a "supercharged" getScript that would handle the plumbing involved in script loading chaining. I mean, all plugins today use a closure so would it be a big deal to use this new getScript instead? I think not.

You could have something like: getScript("url1" , "url2" , callback1 , "url3" , "url4" , 0 ,"url5", finalCallback) where url1 & 2 would be loaded concurrently, then callback1 is called, then url3 & 4 are loaded concurrently,then url5, then the final callback is called. Replace '0' with any value you see fit for a synchronization barrier (or, why not, a special character in front of the next url). User code in callbacks is still free to bind on the ready event, but not forced to and no ajax-code gets shuffled into core.

My 2 cents though.

@jquery
Copy link
Collaborator

@jquery jquery commented on a5b2940 Dec 17, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iamnoah: The remote script is loading but is being prevented from executing.

@aheckmann: To a degree, yes. Obviously we're taking it one step at a time and tackling the harder overall problem first. Baked in to the first version is the concept of url rewriting so it'll definitely be possible to do what you're talking about. It would work something like this:

jQuery.require.namespace.plugin = "http://plugins-cdn.jquery.com/";
jQuery.require("plugin.ajaxForm", "plugins.otherPlugin");

Being able to load CSS as part of the complete package would be great - a good task for after 1.4.0 comes out.

@aheckmann and jaubourg: Yes, it's likely that there would be a FOUC using this technique - if you want to avoid that then you probably shouldn't be doing dynamic script loading to begin with - just include the scripts into the page directly and that'll prevent all display and freeze the browser. I agree with 'jamespadolsey' that the only reliable technique is to do CSS/class manipulation.

@jaubourg: First point: "For starter, everything javascript doesn't revolve around the DOM." That's true - but virtually everything that happens in jQuery does - and that's the point. This is a require script specifically designed to benefit jQuery users and the way jQuery code works.

Second: I really don't understand your example - if everything is in the cache then it's going to load very quickly. If you have a require inside of a require'd file, that won't matter, it'll still be hitting the cache and still be loading quickly.

Third: So just like how current scripts work, only without a frozen UI, seems reasonable. We can definitely work in some timeout logic at a later time but it wouldn't really seem to make sense: By its very definition a file that is required for the execution of the site to work wasn't loaded, therefore the site shouldn't be displayed.

Fourth: It would probably just be easier if you didn't think of it as an Ajax methods - it uses some similar techniques (using XHR or doing dynamic script creation) but it fundamentally isn't part of the Ajax architecture - in fact, in many ways it would actively ignore whatever the user would specify anyway (it will always try to hit the cache, it will ignore the async settings, it can't enforce any accept or content-type settings, etc.).

If you want to think of this as a supercharged getScript, you can, but in many ways it's not. It's a better way of dynamically loading files in a way that will allow pages to start displaying content faster and handle dependencies in a way that won't block script execution and freeze the browser. Having this be in jQuery core (not in Ajax) is critical. Using nothing but the core jQuery file you will be able to load the rest of jQuery dynamically. Requiring the user to load the entirety of the Ajax module would be counter-productive, especially if require won't use much of its functionality (in fact, none at all).

@jeresig
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iamnoah: The remote script is loading but is being prevented from executing.

@aheckmann: To a degree, yes. Obviously we're taking it one step at a time and tackling the harder overall problem first. Baked in to the first version is the concept of url rewriting so it'll definitely be possible to do what you're talking about. It would work something like this:

jQuery.require.namespace.plugin = "http://plugins-cdn.jquery.com/";
jQuery.require("plugin.ajaxForm", "plugins.otherPlugin");

Being able to load CSS as part of the complete package would be great - a good task for after 1.4.0 comes out.

@aheckmann and jaubourg: Yes, it's likely that there would be a FOUC using this technique - if you want to avoid that then you probably shouldn't be doing dynamic script loading to begin with - just include the scripts into the page directly and that'll prevent all display and freeze the browser. I agree with 'jamespadolsey' that the only reliable technique is to do CSS/class manipulation.

@jaubourg: First point: "For starter, everything javascript doesn't revolve around the DOM." That's true - but virtually everything that happens in jQuery does - and that's the point. This is a require script specifically designed to benefit jQuery users and the way jQuery code works.

Second: I really don't understand your example - if everything is in the cache then it's going to load very quickly. If you have a require inside of a require'd file, that won't matter, it'll still be hitting the cache and still be loading quickly.

Third: So just like how current scripts work, only without a frozen UI, seems reasonable. We can definitely work in some timeout logic at a later time but it wouldn't really seem to make sense: By its very definition a file that is required for the execution of the site to work wasn't loaded, therefore the site shouldn't be displayed.

Fourth: It would probably just be easier if you didn't think of it as an Ajax methods - it uses some similar techniques (using XHR or doing dynamic script creation) but it fundamentally isn't part of the Ajax architecture - in fact, in many ways it would actively ignore whatever the user would specify anyway (it will always try to hit the cache, it will ignore the async settings, it can't enforce any accept or content-type settings, etc.).

If you want to think of this as a supercharged getScript, you can, but in many ways it's not. It's a better way of dynamically loading files in a way that will allow pages to start displaying content faster and handle dependencies in a way that won't block script execution and freeze the browser. Having this be in jQuery core (not in Ajax) is critical. Using nothing but the core jQuery file you will be able to load the rest of jQuery dynamically. Requiring the user to load the entirety of the Ajax module would be counter-productive, especially if require won't use much of its functionality (in fact, none at all).

@simonw
Copy link

@simonw simonw commented on a5b2940 Dec 17, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it completely unavoidable to have jQuery.require() work synchronously if called after the DOM load event? I'd be interested in using this mainly for lazy loading of functionality (the first time the user mouses over a static map, load in the dynamic map code) - if synchronous loads block the whole browser UI that's not going to work.

@jaubourg
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeresig:

First point:

I use jQuery heavily in production and I have a SHITLOAD (and I do mean SHITLOAD) of work I do BEFORE the ready event. My server delivers json encoded data into javascript variables (which is much more efficient than having an ajax call and client/server round trip) that I then treat client-side for client specific needs (that way the server code stays clean). Assuming jQuery users only use jQuery's DOM related code and that they do it right away is very far-fetched imo: type control (isFunction,...), ajax (for the most part) and probably several other parts of jQuery are totally DOM independant and, in my experience at least, are generally used BEFORE DOM manipulation features (ie. plugin initialization).

Second point:

I'll try to make it clearer so that you see you'll end up with the opposite of your goal in term of page load speed:

  • in module1.js, I require a yahoo api (say myYahooAPI.js?api_key=MY_API_KEY)
  • in module2.js, I require another api from a third party (say myThirdParty.js?api_key=OTHER_API_KEY&timestamp_is_required_for_private_key_computation=TIMESTAMP)

First time you load the page, module1 & module2 are not in cache so chances are, you'll hit the ready event BEFORE the requires in them gets called : no blocking.
Next time, module1 & module2 are in cache. They are executed as soon as the script tag is parsed. The requires are executed but problem is none of the scripts referred to are cachable (check with yahoo's scripts and most of API_KEY powered services if you don't believe me). So now, you have to wait for both third party scripts to be loaded EVERY TIME you load the page. If the second one is down, the ready event will never be notified, and module1, while having all its dependencies at hand, will never be executed.

Not only is this (very classic) scenario sure to make your page load slower than usual but the behavior changes if your modules are in cache or not (first time, one module gets executed, second time, none of them). Furthermore, this is not what I call degrading gracefully, far from it.

I won't even get into the fact local files will be sync which can mean being blocked like crazy when your server-code is in PHP and you use sessions.

So I repeat it once again: a general purpose dependency system CANNOT block the ready event. As soon as you get out of a minimal unit test, you get into trouble... and fast.

Third point:

See point above and how the implementation you propose makes it completely inconsistent.

Fourth point:

I hear you... then why the hell if $.ajax calling this code rather than providing its own implementation? Byte saving is nice but this makes for an already confused function into a true nightmare.

Fifth point (reponse to your conclusion):

If this is intended for internal use (that is to modularize jQuery) then keep it internal. Because, as general purpose dependency systems go, it comes with too many issues.

But talking about a modular solution for jQuery, I always considered distributing scoped features to the client to be server-side responsibility. Doing it client-side is asking for trouble: too many http requests, serialized loading, cache issues (which mean server configuration issues), cumbersome control code, etc. In the end (and again, I suffered trying so heh), you don't gain much if anything while it's very easy to make things worse. I'd much rather see a properly done php/java/whatever solution than what I'm seeing here.

But anyway, if the idea is to distribute core and having user code add requires for further modules, then, I'm sorry to say so, but there is NO WAY you'll get faster by loading 3 or 4 js files as opposed to all of jQuery in one minified/gzipped request from google. The library is not big enough for that.

@davidwparker
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been waiting for this feature for quite a while and now I'm glad I didn't roll my own. Looking forward to using it! Keep up the great work jQuery team!

@alfborge
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@simonw Might be I misunderstood, but seems to me the answer is in the bullet list at the beginning of this discussion: "Supports specifying a callback (in which case it'll always be async)"

@jeresig
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@simonw: You absolutely can avoid going synchronous - just specify a callback to the require statement and stick your logic that depends upon it in there. Or if you want to load it async and don't care about when the script has finished loading, just continue using getScript.

@jauborg: In your example module2.js will only remain in the cache if you've specifically told the browser to cache it (again, jQuery isn't doing anything special here). If you're telling the browser to cache a dynamically generated file then, naturally, the second script won't load correctly on future loads. If you want to avoid loading the dynamic API scripts on every page load then there's nothing that can be done to avoid that - neither on jQuery's end or just by including the directly in your page. Nothing that you mention will cause the page to "load slower" then what happens now nor will it have a more adverse effect then what currently happens (the page will continue to try loading indefinitely and eventually just throw a script exception). In fact, the situation can end up being much more graceful with require: A timeout could be provided and information could actually be provided to the user in a useful manner letting them know what went wrong.

This has nothing to do with byte saving - the functionality that was in $.ajax() (specifically used for script loading and getScript) was completely duplicated - and had no reliance upon the $.ajax() internals. In fact there was little reason for the code to actually be in core $.ajax() script.

The dynamic loading of jQuery core isn't the sole use case for this feature (obviously) but it's a nice side effect. Being able to selectively load only a couple portions of the code base, dynamically, as you need them is both a huge savings in bandwidth but also in parse time. Steve Souders has written about the advantages of dynamic script loading extensively: Supporting it and integrating into the workflow of jQuery will bring the advantages of this technique to a much larger audience.

I also agree that in the vast majority of situations it makes much more sense to load all of jQuery from a single, highly-cached, file. That is still the optimal way of loading jQuery in desktop browsers with a good internet connection.

@iamnoah
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeresig How can you load a remote script by script tag injection but delay execution? e.g.,

require('http://otherdomain.com/foo.js');
require('http://otherdomain.com/bar.js');
require('baz.js');

baz.js will always load after foo and bar, but bar could load before foo unless you wait until foo has loaded before you inject the script tag for bar. But require doesn't do that, it injects the script immediately, right? What magic am I missing?

@jeresig
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iamnoah: Scripts are injected immediately - at the moment (that's one of the bugs that I noted in the original comment at the start of this thread). At this very moment

 $.require('http://otherdomain.com/foo.js');
 $.require('http://otherdomain.com/bar.js');

Won't be guaranteed to run in the right order but it will before $.require() lands final. However, I should note that currently the following will work and be queued correctly:

 $.require('http://otherdomain.com/foo.js');
 $.require('baz.js');

It's only the case where a second (or third, etc.) external script is being loaded. But as I mentioned, I'll be landing a fix for this.

@iamnoah
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeresig Ah, good, I'm not crazy :)

I do want to point out the pathological nested requires case, e.g.,

$.require('http://otherdomain.com/foo.js');
   // inside foo.js -> $.require('bar.js');
$.require('baz.js');

So a script tag is injected for foo.js immediately, qux.js is queued while it loads*, then once foo.js runs, bar.js is queued. So the execution order will be: foo, baz, bar, instead of the expected foo, bar, baz.

Corner case designed to break the system: yes
Will someone file an issue for it: probably

* except in Safari 3.2.1, where it seems that script tag injection blocks.

@jrburke
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some feedback from someone else that does script loaders in Dojo and RunJS:

  1. The XHR to inlined script will make it hard to debug scripts -- error lines will be weird and hard to match to the source.

  2. Scripts added via appendChild can execute out of DOM order even before page load.

In general I think assuming out of order script tag evaluation is the safest assumption, and always assume async. With those assumption, I believe you can remove the XHR branch completely and always ask for a callback to the require function.

One of the goals with the CommonJS approach to loading is avoiding modules declaring globals. This allows for multiple versions of a module to be loaded in a page. If you feel like you will want this support in jQuery (it would remove the need for things like noConflict), then you will likely want to specify module names inside scripts that get loaded via require, if they want to participate in the multiversion/no globals approach. The loader entry point function still needs to be global, but that is a reduced set of functionality, and can be made to upgrade if multiple loader function names are loaded in a page.

At that point, you will need a loader more like RunJS:
http://code.google.com/p/runjs/wiki/RunJs

If you feel like you want to coordinate on a loader, I am happy to help. If you think RunJS or a subset would be useful to use, I am happy to help with that. I was already planning on moving the project to GitHub, and I can convert the tests to QUnit if that helps things. We could also talk about what kind of code you may or may not want. RunJS has some things for page load detection for instance. It also supports module modifiers, which you may or may not find useful. The modifiers could be moved to a RunJS plugin like the i18n and text file support.

@jaubourg
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeresig

Seeing as we seem not to talk the same language: http://github.com/jaubourg/forjohn ;)

@jeresig
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jaubourg: I think we're on the same page now, there are definitely some issues that you mention that haven't (yet) been resolved and would like to before the final release. In general I don't have as much of a problem as you do with delaying the ready event - I think it works well for a very specific case (making sure that the user can see something sooner). I talked with Steve Souders today and he tends to agree.

As it stands though it's simply too close to the wire to land this and have it really match expectations. I'm going to push $.require to post-1.4 territory. Thanks again for your tests, jaubourg. Let's sit down together post-1.4 and really hash out the new ajax code.

@jaubourg
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeresig: heh, thank YOU for all the work you do.

Anyway, call me Julian, I feel like a sweedish brand name for a Kitchen Aid wannabe when you call me by my screen name :P

As for the new ajax code, It needs to provide a full xhr emulation (finally know how I'll do it properly) given how requests regarding the xhr and it's behaviour have been flying around (seems like people actually do re-use the xhr which I find dirty but meh). I'm also concerned with code size explosion but I'm confident you'll have gazillions of ideas to deal with it.

As a side note, I'm working on a loader with dependencies (damn you for putting ideas in my head and depriving me of sleep :P). Will probably land it as a plugin ;)

@aheckmann
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jaubourg : Another thanks for the great follow up tests around the FOUC. I'll be publishing my loader (http://github.com/aheckmann/jQuery.use) over the next few days too as I feel more and more up to it (just coming back from major surgery).

@rmanalan
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeresig can you update us on the status of this? Is it usable in this branch and what post-1.4 release can we expect to see it in? Thanks!!!

@jeresig
Copy link
Member Author

@jeresig jeresig commented on a5b2940 Feb 5, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be usable but no, it's not part of jQuery core yet. I want to make sure that we work out the exact API before moving further - so I don't know exactly what release it'll land in, yet.

@Marak
Copy link

@Marak Marak commented on a5b2940 Jun 29, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeresig

Are you thinking about CommonJS stuff at all for this?

Something like:

 var myModule = require('foo.js'); 

and then have the exports in foo.js get assigned to the myModule variable (instead of automatically polluting scope with your require)

it might be cool (albiet out of scope of require) to have something like this.

@getify
Copy link

@getify getify commented on a5b2940 Jun 29, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, the challenge of loading two scripts in parallel but guaranteeing the execute in order is something I implemented a hack for into LABjs. FF and Opera will automatically execute scripts in the order they are added to the DOM, regardless of who loads first. But IE, Chrome, and Safari do not make that guarantee on dynamically inserted script elements.

For Opera and FF, LABjs just adds the script elements in order and relies on the browser to do its thing. For IE, Chrome, and Opera, LABjs uses a hack to first "pre-cache" all the resources (even remote ones), and then go back and re-add them to the DOM in the correct order.

As far as I am aware, this is the only truly cross-browser way to dynamically load scripts in parallel but ensure their execution order.

@Marak
Copy link

@Marak Marak commented on a5b2940 Jun 29, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think http://sexyjs.com can do what you are saying getify, but really the only thing I haven't seen implemented "right" anywhere is the ability to seamless require CommonJS modules on the browser and have all the namespaces sort themselves out. RequireJS kinda does this, but isn't as seamless as I'd like to see.

@jrburke
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marak: unfortunately the priority for CommonJS was not in-browser execution but something that worked well in server/desktop envs that have access to synchronous IO calls, which allows them to process require() calls immediately upon seeing them. Unfortunately for the browser, the best performing/easiest to debug IO is asynchronous via script tags. I go into some design choices for a browser loader here that dive into why async script tags in the browser are the best choice:
http://requirejs.org/docs/why.html

CommonJS has some "transport" proposals to help send CommonJS modules to the browser, and RequireJS has an adapter that supports the Transport D proposal, and this is used in Transporter:
http://github.com/kriszyp/transporter

RequireJS also has a converter script that can convert CommonJS modules to RequireJS format, and RequireJS follows the same scoped ideals and module naming that CommonJS supports. I believe those two options (supporting a transport format and allow module conversion) are the best paths to reusing CommonJS code in the browser, particularly since RequireJS can also run in other JS environments like Rhino and Node.

I have a fork of jQuery that does not include require() from RequireJS, but just adds enough modifications to allow it to be treated as a module and to hold off calling document ready callbacks until all scripts are loaded, you can see it here:
http://github.com/jrburke/jquery/commit/0178972afb7c125b36c690d483be84cbff0fcc61

I believe that is likely to be a better approach to building a require into jQuery itself, since ideally a require call could load jQuery. Other script loaders could use the same modifications to support loading that modified jQuery, as long as the other script loader defined a require.def function, a require.s.isDone boolean and called a require.callReady function when all scripts are finished loading. If it is helpful to generalize the names a bit more, I am open to that. It would allow script loaders to iterate faster than something built into jQuery, and allow different types of loaders (for instance ones that care to support CommonJS code vs ones that do not).

@Marak
Copy link

@Marak Marak commented on a5b2940 Jun 29, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jrburke -

thanks for the detailed reply. i actually found http://requirejs.org/docs/release/0.11.0/minified/require-jquery-1.4.2.js a few hours ago and have been experimenting further with RequireJS.

i have the need for a browser-side require() in a few places each with slightly different implications. the biggest one i'm trying to figure out is a way to run node.js code natively in the browser without any modifications. i've got a great start, but the require stuff can get a little mindbending (pretty much stuck on the fact that the node.js require is synchronous.

if i load all my dependancies ahead of time, i can run node.js code without modification. the issue arises when i don't preload everything. check out this simple example:

// dual-demo.js

 var sys = require('sys'),
 translate = require('./translate');

 translates.text('foo', function(rsp){
   sys.puts(rsp);
 });

since this is no callback in node for require(), it kinda is fucked on the browser. the current solution is to determine all your requires ahead of time (and load them into memory), but that isnt very efficient or succinct. i'd like to be able to require('./dual-demo') and have it just work.

i'm sure i'll be following up with you more on the github page for RequireJS.

@getify
Copy link

@getify getify commented on a5b2940 Jun 29, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, this is what's wrong with trying to have a single API that does both local synchronous file system loads of modules and one that is remote and asynchronous. There's only one mechanism that can handle that well, and that's promises. But that's a chicken-and-the-egg because the Promises system is itself a module that you have to require(), rather than it being an inherent part of the system (like require is) that can be used for stuff like this.

I advocate having different loading code for server-side and browser-side as I think they are very different paradigms. But I understand the attraction to finding a syntax that does both.

The ideal (again IMHO) might be something like:

require("somemodule")
.then(function(M){
   // use that module as M
});

But again, the Promises system to make something like that work would need to be core to the environment in the SSJS and pre-loaded in the browser to be able to give a definition to require(). I think this starts to get into nasty recursion on itself because you have the question, "how do i load the loader?" (or rather, "how do i require the require()er?")

@jrburke
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getify: I am not sure Promises gets all the way to the solution. Some modules depend on multiple modules, so the then() callback should only be fired if they are all available, or you end up with possibly many nested promise.then() calls, which to me is worse than:

require(["one","two"], function(){});

where there is just one function callback for when all the modules are available. If the promise returned from the require call would know to wait for multiple resources, then the syntax would look a lot like the require example above, but wordier (add in a then call). Wordiness was one of the main reasons for CommonJS to adopt the syntax they have now.

The problem could have been solved by CommonJS adopting an async, function callback format that could work in the browser. Even though it has an extra function call to type out, for multiple dependencies, the typing starts to even out.

Even though CommonJS did not use a function callback, there is value in still adopting other parts of their format, specifically, encouraging getting dependencies via a function call -- require() -- and using a string name for the dependency instead of depending on global variable pollution for things to work. I also think the module names used in CommonJS are more portable than plain URLs -- the modules can be bundled together in one file and still be able to reference the modules correctly without having to change the references, and it is easier to map the module names to other URLs.

@getify
Copy link

@getify getify commented on a5b2940 Jun 29, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jrburke -- IMHO, one of the great parts about Promises, especially the chained kind that I'm in favor of, is the ability to do exactly that... the main Promise could be "interrupted" of sorts while a sub-promise is still being fulfilled, and so on. I think it makes this kind of thing quite graceful. But that's just me. :)

Please sign in to comment.