/** * Async JavaScript Loader * https://github.com/th507/asyncJS * * Slightly Deferent JavaScript loader and dependency manager * * @author Jingwei "John" Liu */ (function(name, context) { /*jshint plusplus:false, curly:false, bitwise:false, laxbreak:true*/ "use strict"; // some useful shims and variables var dataURIPrefix = "data:application/javascript,"; // do not record return value for asynchronous task // if handler is OMITTED var OMITTED = "OMITTED"; // for better compression var Document = document; var Window = window; var ArrayPrototype = Array.prototype; // detect Data URI support var supportDataURI = true; // As much as I love to use a semantic way to // detect Data URI support, all the detection // methods I could think of are asynchronous, // which makes them less reliable when calling // asyncJS immediately after its instantiation // IE 8 or below does not support Data URI. // IE 8 or below returns false // http://tanalin.com/en/articles/ie-version-js if (Document.all && !Document.addEventListener) { supportDataURI = false; } /** * @private * @name getCutoffLength * Get cut-off length for iteration * * @param {Array} arr * @param {Number} cutoff */ function getCutoffLength(arr, cutoff) { //because AsyncQueue#then could add sync task at any time // we must read directly from this.tasks.length var length = arr.length; if (~cutoff && cutoff < length) length = cutoff; return length; } /** * @private * @name timeout * Run callback in setTimeout * * @param {Function} fn */ function timeout(fn, s) { Window.setTimeout(fn, s || 0); } /** * @private * @name immediate * Run callback asynchronously (almost immediately) * * @param {Function} fn */ var immediate = Window.requestAnimationFrame || Window.webkitRequestAnimationFrame || Window.mozRequestAnimationFrame || timeout; /** * @private * @name throwLater * Throw Error asynchronously * * @param {Object} error */ function throwLater(error) { timeout(function() { throw error; }); } /** * @private * @name isURL * Check if str is a URL * * @param {String} str */ function isURL(str) { // supports URL starts with http://, https://, and // // or a single line that ends with .js or .php return ( /(^(https?:)?\/\/)|(\.(js|php)$)/.test(str) && !/(\n|\r)/m.test(str) ); } /** * @private * @name isFunction * Check if fn is a function * * @param {Function} fn */ // This is duck typing, aka. guessing function isFunction (fn) { return fn && fn.constructor && fn.call && fn.apply; } /** * @private * @name makeArray * Make an array out of given object * * @param {Object} obj */ function makeArray(obj) { var isArray; if ((isArray = Array.isArray)) { return isArray(obj) ? obj : [obj]; } return ArrayPrototype.concat(obj); } /** * @private * @name slice * Convert array-like object to array * * @param {Object} arr */ function slice (arr) { return ArrayPrototype.slice.call(arr); } /** * @private * @name factory * Factory Method producing function * that receives reduced arguments * * @param {Function} fn */ // http://wiki.ecmascript.org/doku.php?id=conventions:safe_meta_programming var call = Function.call; function factory() { var defaults = slice(arguments); return function() { // keep this as simple as possible return call.apply(call, defaults.concat(slice(arguments))); }; } // end of shims /** * @private * @name resolveScriptEvent * Script event handler * * @param {Object} resolver * @param {Object} evt */ function resolveScriptEvent(resolver, evt) { /*jshint validthis:true */ var script = this; // run only when ready // script.readyState is completed or loaded if (script.readyState && !(/^c|loade/.test(script.readyState)) ) return; // never rerun callback if (script.loadStatus) return; // unbind to avoid rerun script.onload = script.onreadystatechange = script.onerror = null; script.loadStatus = true; if (evt && evt.type === "error") { var src = script.src || "Resource", fails = " fails to load."; // custom error // TODO: create a more specific stack for this Error var error = { name: "ConnectionError", source: src, evt: evt, stack: src + fails, message: fails, toString: function() { return this.source + this.message; } }; throwLater(error); resolver.reject(error); return; } resolver.resolve(); } /** * @private * @name appendScript * Append asynchronous script to DOM * * @param {String|Function} str * @param {Object} resolver */ function appendScript(str, resolver) { var ScriptTagName = "script"; var script = Document.createElement(ScriptTagName); // at least one script could be found, // the one which wraps around asyncJS var scripts = Document.getElementsByTagName(ScriptTagName); var lastScript = scripts[scripts.length - 1]; script.async = true; script.src = str; if (!resolver) return; // executes callback if given script.loadStatus = false; var resolveScript = factory(resolveScriptEvent, script, resolver); // onload for all sane browsers // onreadystatechange for legacy IE script.onload = script.onreadystatechange = script.onerror = resolveScript; // inline script tends to change nearby DOM elements // so we append script closer to the caller // this is at best a ballpark guess and // might not work well with some inline script var slot = lastScript; // in case running from Console // we might encounter a scriptless page slot = slot || document.body.firstChild; slot.parentNode.insertBefore(script, slot); } /** * @private * @name loadFunction * Loads JS function or script string for * browser that does not support Data URI * * @param {String|Function} js * @param {Function} fn */ function loadFunction (js, resolver) { immediate(function () { try { js.call(null, resolver); } catch (e) { resolver.reject(e); } }); } /** * @private * @name load * Loads one request or executes one chunk of code * * @param {String|Function} js * @param {Function} resolve */ function load(js, resolver) { /*jshint newcap:false, evil:true*/ // js is not a function if (!isFunction(js)) { if (isURL(js)) { appendScript(js, resolver); return; } if (supportDataURI) { // wraps up inline JavaScript into external script js = dataURIPrefix + encodeURIComponent(js); appendScript(js, resolver); return; } } var fn = isFunction(js) ? js : Function(js); // a synchronous function is wrapped into a special function // so that we could use the same logic as an asynchronous function if (!resolver.async) { var task = fn; fn = function (resolver) { try { task.call(null); resolver.resolve(); } catch (e) { resolver.reject(e); } }; } loadFunction(fn, resolver); } /** * @public * @name AsyncQueue * Create a semi-Promise for asyncJS * @constructor * * @param {Array|String|Function} tasks * @param {Function} fn */ function AsyncQueue(tasks, fn) { // better compression for shrinking `this` var self = this; // TODO: exposing this is not safe self.tasks = []; self.callbacks = []; self.errors = []; // return values of Promise self.data = {}; // resolved task index self.nextTask = 0; // resolved callback index self.nextCallback = 0; // -1 (default) means not waiting for AsyncQueue#then self.until = -1; // queue is executing callback self.digest = false; // add tasks and callbacks self.add(tasks).whenDone(fn); } /** * @private * @name resolveCallback * Resolve next asyncJS callback */ function resolveCallback() { /*jshint validthis:true*/ var self = this; // if current digestion circle is still active // then try again later if (self.digest) { timeout(factory(resolveCallback, self), 50 / 3); return; } self.digest = true; var fn, next, i = self.nextCallback; // always update length for next iteration for (; i < getCutoffLength(self.callbacks, self.until); i++) { if (self.nextTask !== self.tasks.length) continue; next = self.nextCallback; fn = self.callbacks[next]; if (fn) { self.nextCallback = i + 1; // passing in current taskIndex fn.call(null, self.data, self.nextTask - 1, self.errors); // if callback is not to generated function // then it would advance to the next iteration if (!fn.untilThen) continue; // reduce nextCallback count self.nextCallback--; // release iteration lock self.until = -1; } // remove invalid or untilThen function self.callbacks.splice(next, 1); } self.digest = false; } /** * @private * @name nextTick * Advance to next tick in the queue * For AsyncQueue#reject or AsyncQueue#resolve * * @param {String} handle * @param {Object} data */ function nextTick() { /*jshint validthis:true*/ var self = this; // never resolve when tasks are finished if (self.nextTask < self.tasks.length) { // if tasks are still queueing // increment nextTask if (++self.nextTask !== self.tasks.length) return self; } // check callbacks if all tasks are finished resolveCallback.call(self); return self; } /** * @private * @name resolve * Resolve next asyncJS queue * Normally, you never have to call this * * @param {String} handle * @param {Object} data */ AsyncQueue.prototype.resolve = function (handle, data) { /*jshint validthis:true*/ var self = this; // save data if available and necessary if (handle && handle !== OMITTED) self.data[handle] = data; return nextTick.call(self); }; /** * @private * @name reject * Reject and continue next asyncJS queue * * @param {Object} error */ AsyncQueue.prototype.reject = function (error) { /*jshint validthis:true*/ var self = this; if (error) { throwLater(error); self.errors.push(error); } // keep executing other stacked tasks return nextTick.call(self); }; /** * @public * @name AsyncQueue#whenDone * Attach extra callback to next asyncJS queue * * @param {Function} fn */ AsyncQueue.prototype.whenDone = function(fn) { // save a few bytes var self = this; if (!fn) return self; // tasks undone if (self.nextTask > self.tasks.length) return self; // add callback function self.callbacks.push(fn); // try resolve if (self.nextTask === self.tasks.length) self.resolve(); return self; }; /** * @public * @name AsyncQueue#add * Add tasks to next asyncJS queue * * @param {Array|String|Function} tasks */ AsyncQueue.prototype.add = function(tasks, handle) { var self = this; if (!tasks) return self; // warn user if returned data could overwrite // existing data, without stopping further execution if (handle && self[handle]) { var error = new Error("Callback value name: " + handle + " is registered"); throwLater(error); self.errors.push(error); } tasks = makeArray(tasks); var resolver = { resolve: factory(self.resolve, self, handle), reject: self.reject, async: !!handle }; for (var i = 0, fn; i < tasks.length; i++) { fn = tasks[i]; if (!fn) continue; // this is just for future reference self.tasks.push(fn); // resolve function load(fn, resolver); } return self; }; /** * @public * @name AsyncQueue#then * Add a SINGLE dependent task to next asyncJS queue * which blocks all following callbacks * until this task is finished * * @param {Array|String|Function} task */ AsyncQueue.prototype.then = function(task, handle) { var self = this; if (!task) return self; // if there are still tasks unfinished // add new tasks when this function // that has a `untilthen` property function addDependent() { // when `resolveCallback` sees the // property, it will stop executing // all other callbacks until it is done self.until = self.nextCallback; self.add(task, handle); } addDependent.untilThen = true; return self.whenDone(addDependent); }; /** * @public * @name asyncJS * Loads multiple requests or executes inline code * * @param {String|Array} js * @param {Function} fn * * @return {Object} asyncJS queue */ function asyncJS(js, fn) { return new AsyncQueue(js, fn); } // export asyncJS /*jshint node:true*/ /*global define*/ if (typeof module !== 'undefined' && module.exports) { module.exports = asyncJS; } else if (typeof define === "function" && define.amd) { define(function(){ return asyncJS; }); } else { context[name] = asyncJS; } }("asyncJS", this));