JavaScript Asynchronous Dependency Loader

JavaScript Software Design 6 min read
ui-image JavaScript Asynchronous Dependency Loader
Image generated by ChatGPT

Back in 2013, AMD was the standard approach for non-blocking script loading. It works well, but requires every dependency to be wrapped as an AMD module. That’s fine for your own code. For third-party libraries, it means patching external code just to satisfy a module format, which isn’t a trade-off worth making.

So I built a small loader that works around this. Non-AMD libraries expose their API via globals, and all we really care about is knowing when they’ve finished loading. The loader fires a custom event when each script loads, and your code declares which events it needs before running. No module wrapping required.

For your own code, a CommonJS-style pattern keeps the namespace clean:

(function( exports ){
  exports.moduleA = { /*..*/ };
  exports.moduleN = { /*..*/ };
}( module ));
console.log( typeof module.moduleA );

The API:

// Load /js/form.js asynchronously and register it as a dependency named "form"
rjs.define("/js/form.js", "form");
// Invoke the handler once both DOMContentLoaded and the "form" dependency have fired
rjs.require(["DOMContentLoaded", "form-loaded"], function( module ){
});

Unlike AMD, rjs.require takes event names rather than module identifiers. This makes the intent explicit: you’re declaring what needs to happen before your code runs, not what it imports. Any DOM event can appear in the list alongside dependency-loaded events.

The vanilla JS implementation (ES5):

var module = module || {},
    rjs = (function( global, exports ){
   "use strict";
    var document = global.document,
        queue = {},
        /**
         * Firing event helper
         *
         * @param {string} eventName
         * @param {array} payload
         */
        triggerEvent = function( eventName, payload ) {
            var e = document.createEvent("Event");
            e.initEvent( eventName, true, true );
            e.payload = payload;
            document.dispatchEvent( e );
        },
        /**
         * Subscribe helper to an event helper
         *
         * @param {string} eventName
         * @param {function} fn
         */
       onEvent = function( eventName, fn ) {
            document.addEventListener( eventName, function( e ){
                fn.apply( null, e.payload || [] );
            }, false );
        },
       eventQueue = (function(){
           return new function() {
                var queue = [];
                return {
                    /**
                     * Register a supplied event to the queue
                     *
                     * @param {string} eventName
                     */
                    register: function( eventName ) {
                        queue.indexOf( eventName ) === -1 && queue.push( eventName );
                    },
                    /**
                     * Checks if all the events of a supplied array already registered
                     * in the queue
                     *
                     * @param {array} events
                     */
                    isResolved: function( events )  {
                        var i = 0, len = events.length;
                        if (!len) {
                            return false;
                        }
                        for ( ; i < len; i++ ) {
                            // If at least one event of the supplied list is not
                            // yet fired, the queue is not resolved
                            if ( queue.indexOf( events[ i ] ) === -1 ) {
                                return false;
                            }
                        }
                        return true;
                    }
                }
            };
       }());

       return (function(){
            // Register DOMContentLoaded event
           onEvent( "DOMContentLoaded", function(){
               eventQueue.register("DOMContentLoaded");
           });
           return {
                /**
                 * Load a given script asynchronously and fires event <dependency>-load
                 * when script is loaded and both DOM are ready and script is loaded
                 *
                 * @param {string} file - dependency script path
                 * @param {string} dependency - dependency name
                 * @param {function) completeFn OPTIONAL - A callback function
                 *      that is executed when the request completes.
                 */
                define: function( file, dependency, completeFn ) {
                    var script = document.createElement("script");
                    script.type = "text/javascript";
                    script.src = file;
                    document.body.appendChild( script );
                    script.addEventListener( "load", function(){
                        var eventName = dependency + "-loaded";
                        // Registers event in the queue
                        eventQueue.register( eventName );
                        // Fires event when the script is loaded
                        triggerEvent( eventName, [ exports ] );
                        completeFn && completeFn();
                    }, false );
                },
                /**
                 * Call the function fn when all supplied events fired
                 *
                 * @param {array} dependencies
                 * @param {function} fn - callback function that is executed
                 *  when all the supplied dependencies resolved
                 */
                require: function( events, fn ) {
                     // Event fired before a handler subscribed
                    if ( eventQueue.isResolved( events ) ) {
                         return fn( module );
                    }
                    // Event fired after a handler subscribed
                    events.forEach(function( eventName ){
                        onEvent( eventName, function( module ){
                            eventQueue.isResolved( events ) && fn( module );
                        });
                    });
                }
           };
       }());

}( this, module ));

A jQuery port is also available. The library’s event helpers reduce the boilerplate and add legacy browser support:

var module = module || {},
    rjs = (function( global, exports ){
   "use strict";
    var document = global.document,
        $ = global.jQuery,
        queue = {},
       eventQueue = (function(){
           return new function() {
                var queue = [];
                return {
                    /**
                     * Register a supplied event to the queue
                     *
                     * @param {string} eventName
                     */
                    register: function( eventName ) {
                        $.inArray( eventName, queue ) === -1 && queue.push( eventName );
                    },
                    /**
                     * Checks if all the events of a supplied array already registered
                     * in the queue
                     *
                     * @param {array} events
                     */
                    isResolved: function( events )  {
                        var i = 0, len = events.length;
                        if (!len) {
                            return false;
                        }
                        for ( ; i < len; i++ ) {
                            // If at least one event of the supplied list is not
                            // yet fired, the queue is not resolved
                            if ( $.inArray( events[ i ], queue ) === -1 ) {
                                return false;
                            }
                        }
                        return true;
                    }
                }
            };
       }());
       return (function(){
            // Register DOMContentLoaded event
           $( document ).on( "DOMContentLoaded", function( ){
               eventQueue.register("DOMContentLoaded");
           });
           return {
                /**
                 * Load a given script asynchronously and fires event <dependency>-load
                 * when script is loaded and both DOM are ready and script is loaded
                 *
                 * @param {string} file - dependency script path
                 * @param {string} dependency - dependency name
                 * @param {function) completeFn OPTIONAL - A callback function
                 *      that is executed when the request completes.
                 */
                define: function( file, dependency, completeFn ) {
                    var script = document.createElement("script");
                    script.type = "text/javascript";
                    script.src = file;
                    document.body.appendChild( script );
                    $( script ).on( "load", function(){
                        var eventName = dependency + "-loaded";
                        eventQueue.register( eventName );
                        // Fires event when the script is loaded
                        $( document ).trigger( eventName, [ exports ] );
                        completeFn && completeFn();
                    });
                },
                /**
                 * Call the function fn when all supplied events fired
                 *
                 * @param {array} dependencies
                 * @param {function} fn - callback function that is executed
                 *  when all the supplied dependencies resolved
                 */
                require: function( events, fn ) {
                    // Event fired before a handler subscribed
                    if ( eventQueue.isResolved( events ) ) {
                         return fn( module );
                    }
                    // Event fired after a handler subscribed
                    events.forEach(function( eventName ){
                        $( document ).on( eventName, function( e, module ){
                            eventQueue.isResolved( events ) && fn( module );
                        });
                    });
                }
           };
       }());
}( window, module ));

The library is released under MIT license at https://github.com/dsheiko/micro-requirejs