Anthony Su

coding adventures

Additional Things I Learned From jQuery Source

Introduction

Several months ago I decided to build a client side library called Guac. The library supports a similar and trimmed down API of that of jQuery. My library is by no means production ready, but building it was a good exercise for me to also explore the internals of jQuery.

This posts will discuss some of the internal workings of jQuery and explain how these ideas relate to the higher level API of jQuery. I will be walking you through parts of the jQuery codebase.

I would also recommend taking a look at Paul Irish’s related posts: 10 Things I learned From jQuery Source and 11 More Things I learned From jQuery source. Here is also a github gist that summarizes those videos. I try not to overlap with ideas he has already explained unless they are crucial to what I am discussing below.

At the time of this writing, the version of jQuery I am exploring is v2.1.1.

How the development modules are structured

From a development perspective jQuery is broken down into a bunch of modules.

These modules are defined using the Asynchronous Module Definition AMD specification.

The main file of jQuery is src/jquery.js and its relevant dependencies are listed as the array in the define function.

AMD Definition for jQuery
1
2
3
4
5
6
7
8
9
define([
    "./core", // refers to ./core.js
    "./selector", // refers to ./selector.js
    // .. more dependencies
], function( jQuery ) {

return jQuery;

});

This way of defining modules is crucial to how these modules are built. As we will discuss requirejs is used to resolve dependencies and load related modules/files.

Source:src/jquery.js

How jQuery is built

The README.md for jQuery describes at a high level how the jQuery source is built. In particular you will need the npm and grunt. Grunt is a task runner which allows you to automate repetitive tasks from the command line. Grunt also provides a development API, which jQuery uses. In the development directory there is a related Gruntfile that is read when grunt is ran on the command line. After you install npm and grunt you can build jQuery by running npm install && grunt. I’m going to focus on the lower levels on how jQuery is built.

jQuery uses requirejs to build out jQuery by running requirejs as a node module The files that are responsible for building jQuery are stored in the directory build/tasks. These are loaded by the Gruntfile:

Load Build Tasks
1
2
// Integrate jQuery specific tasks
grunt.loadTasks( "build/tasks" );

Source: build/tasks

The build task is executed when grunt is run on the command line. This is because grunt runs the default task, which includes building among another things like checking the JavaScript code quality, minifying the source for distribution, and comparing file size of the current git branch to that of master.

default grunt task
1
2
3
4
// Short list as a high frequency watch task
grunt.registerTask( "dev", [ "build:*:*", "lint" ] );

grunt.registerTask( "default", [ "jsonlint", "dev", "uglify", "dist:*", "compare_size" ] );

Source Code: register default grunt task

The relevant configuration for the tasks are also stored in the Gruntfile as a JavaScript object.

Grunt Configuration for build
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
build: {
    all: {
        dest: "dist/jquery.js",
        minimum: [
            "core",
            "selector"
        ],
        // Exclude specified modules if the module matching the key is removed
        removeWith: {
            ajax: [ "manipulation/_evalUrl", "event/ajax" ],
            callbacks: [ "deferred" ],
            css: [ "effects", "dimensions", "offset" ],
            sizzle: [ "css/hiddenVisibleSelectors", "effects/animatedSelector" ]
        }
    }
}

Source: grunt build configuration

build/tasks/build.js registers a multitask called build that iterates over all targets of build if none are specified when built is run on the command line: grunt build.

register multitask
1
2
3
4
5
6
7
grunt.registerMultiTask(
        "build",
        "Concatenate source, remove sub AMD definitions, " +
            "(include/exclude modules with +/- flags), embed date/version",
        function() {
            // ...
        });

Source Code: build/tasks register build tasks

The important line in build.js is requirejs.optimize(config, function( response ) { ... that uses requirejs’ optimize as function to build out the file.

The configuration config that is passed to require.js optimize is also important to take a look at. A more detailed definition of these options is available here.

configuration for requirejs.optimize build for jQuery
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
config = {
    baseUrl: "src", // all modules are located relative to this path
    name: "jquery",
    out: "dist/jquery.js",
    // We have multiple minify steps
    optimize: "none",
    // Include dependencies loaded with require
    findNestedDependencies: true,
    // Avoid breaking semicolons inserted by r.js
    skipSemiColonInsertion: true,
    // these files are used to prepend and append 
    // to optimized response
    wrap: {
        startFile: "src/intro.js",
        endFile: "src/outro.js"
    },
    // we map the "sizzle" module to its actual source
    paths: {
        sizzle: "../external/sizzle/dist/sizzle"
    },
    rawText: {},
    // A function that will be called for every write to an optimized bundle
    // of modules. This allows transforms of the content before serialization.
    // convert does the heavy lifting on massaging the files :  mainly stripping out the definitions generated by requirejs
    onBuildWrite: convert
};

An important property to note is the property onBuildWrite and the function convert which is a callback that is invoked with the library is built. In particular convert mainly strips out the definitions generated by requirejs.

The optimized contents are then passed to the config.out function which will handle writing the text to the relevant destination.

jQuery’s config.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    /**
    * Handle Final output from the optimizer
    * @param {String} compiled
    */
    config.out = function( compiled ) {
    compiled = compiled
        // Embed Version
        .replace( /@VERSION/g, version )
        // Embed Date
        // yyyy-mm-ddThh:mmZ
        .replace( /@DATE/g, ( new Date() ).toISOString().replace( /:\d+\.\d+Z$/, "Z" ) );

    // Write concatenated source to file
    grunt.file.write( name, compiled );
    };

Source Code:build/tasks/build.js config.out

Lastly there is another task called custom that handles the custom exclusion/inclusion of certain modules.

jQuery Alias

You’re probably familiar $ variable which is often used for selecting DOM Elements like so $("#awesomeID")

In fact, the $ is actual just an alias for jQuery, so you can also use jQuery("#awesomeID") instead of $("#awesomeID").

The related line within the codebase is

jQuery aliasing
1
window.jQuery = window.$ = jQuery;

Source Code: src/exports/global.js

Variables at the global scope are automatically attached to the window object. Similarly if you want to create a global from a variable from an inner scope, add it to the window object.

Since the jQuery codebase is encapsulated in an IIFE to avoid polluting the global namespace, jQuery and $ are attached to the window object to export it as a global.

Export jQuery and $ to Global Scope
1
2
3
4
5
(function() { // Immediately Invoked Function Expression (IIFE) to encapsulate code base
    // ...
    window.jQuery = window.$ = jQuery; // create alias and export global variables
    // ...
})();

Please also note that other libraries such as prototype do also use $ as a variable, so if you are using $ in jQuery and other libraries refer to jQuery’s documentation on avoiding conflicts with other libraries.

Prototype Alias

If you’ve ever tried creating your own jQuery plugin you’ll notice that you add the plugin to $.fn.

As an example for jQuery’s plugin tutorial, creating a plugin called greenify that makes retrieved text green would work like so:

jQuery example plugin: greenify
1
2
3
4
    $.fn.greenify = function() {
        this.css( "color", "green" );
    };
    $( "a" ).greenify(); // Makes all the links green.

In fact creating a plugin actually attaches greenify to jQuery’s prototype, because $.fn is an alias for prototype. Recall that an object inherit methods and properties from their prototype. Since, your plugin is attached to the prototype, you can invoke the plugin after creating your jQuery instance.

The related line that creates this aliasing is as follows:

jQuery.prototype aliasing
1
2
3
jQuery.fn = jQuery.prototype = {
    // ...
}

Source Code: src/core.js

Just to verify that jQuery.fn is indeed an alias for jQuery.prototype, we can check that their memory addresses are equal.

verify jQuery.fn is an alias for jQuery.prototype
1
jQuery.fn === jQuery.prototype // true

Using fn as an alias for prototype is a very convenient shorthand. I believe fn (function) also makes sense semantically because when you are creating a plugin, you are essentially adding another method to the jQuery Object.

Method Chaining

Many of jQuery’s methods return this (referring to the jQuery instance)

For example addClass

addClass returns this
1
2
3
4
    addClass: function( value ) {
        // ...
        return this;
    }

Source Code

Since this is a reference to the jQuery object, when a method returns this, you can immediately invoke another method like so:

Method Chaining Example
1
$(".box").addClass("red").css("opacity", 0.7);

jQuery Constructor

Every time you invoke jQuery or $, you are invoking a constructor. Often times you initialize an object by invoking the constructor with the new keyword. jQuery hides this by calling new jQuery.fn.init(selector, context) internally, when jQuery is invoked.

jQuery Constructor
1
2
3
4
5
6
// Define a local copy of jQuery
jQuery = function( selector, context ) {
    // The jQuery object is actually just the init constructor 'enhanced'
    // Need init if jQuery is called (just allow error to be thrown if not included)
    return new jQuery.fn.init( selector, context );
}

Source Code: src/core.js

Since jQuery.fn.init is the internal constructor that is used to initialize the jQuery object, the prototype of init is set to the prototype of jQuery.

Set fn.init.prototype to jQuery.fn
1
2
3
4
5
// Give the init function the jQuery prototype for later instantiation
init.prototype = jQuery.fn;

// verify that they are indeed equal
jQuery.fn.init.prototype === jQuery.fn && jQuery.fn === jQuery.prototype

Source Code: src/core/init.js

Function Overloading

Many programming languages support a feature known as function overloading
which allows for more than one function of the same name in the same scope as long as they differ by certain criteria. According to this function overloading table from Microsoft, the parts of a function declaration that are used to differentiate between functions within the same scope include number of arguments, type of arguments, presence or absence of ellipsis and const or volatile.

Although this feature is not explicitly supported in JavaScript, John Resig’s describe a method for simulating overloading by number of arguments.

jQuery also supports function overloading by checking the type of arguments and the jQuery() documentation discusses such usage in more detail.

Notice the // HANDLE comments within the source code that account for all the different cases.

jQuery.fn.init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
init = jQuery.fn.init = function( selector, context ) {
        var match, elem;

        // HANDLE: $(""), $(null), $(undefined), $(false)
        if ( !selector ) {
            return this;
        }

        // Handle HTML strings
        if ( typeof selector === "string" ) {
            if ( selector[0] === "<" &&
                selector[ selector.length - 1 ] === ">" &&
                selector.length >= 3 ) {

                // Assume that strings that start and end with <> are HTML and skip the regex check
                match = [ null, selector, null ];

            } else {
                match = rquickExpr.exec( selector );
            }

            // Match html or make sure no context is specified for #id
            if ( match && (match[1] || !context) ) {

                // HANDLE: $(html) -> $(array)
                if ( match[1] ) {
                    context = context instanceof jQuery ? context[0] : context;

                    // Option to run scripts is true for back-compat
                    // Intentionally let the error be thrown if parseHTML is not present
                    jQuery.merge( this, jQuery.parseHTML(
                        match[1],
                        context && context.nodeType ? context.ownerDocument || context : document,
                        true
                    ) );

                    // HANDLE: $(html, props)
                    if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
                        for ( match in context ) {
                            // Properties of context are called as methods if possible
                            if ( jQuery.isFunction( this[ match ] ) ) {
                                this[ match ]( context[ match ] );

                            // ...and otherwise set as attributes
                            } else {
                                this.attr( match, context[ match ] );
                            }
                        }
                    }

                    return this;

                // HANDLE: $(#id)
                } else {
                    elem = document.getElementById( match[2] );

                    // Support: Blackberry 4.6
                    // gEBID returns nodes no longer in the document (#6963)
                    if ( elem && elem.parentNode ) {
                        // Inject the element directly into the jQuery object
                        this.length = 1;
                        this[0] = elem;
                    }

                    this.context = document;
                    this.selector = selector;
                    return this;
                }

            // HANDLE: $(expr, $(...))
            } else if ( !context || context.jquery ) {
                return ( context || rootjQuery ).find( selector );

            // HANDLE: $(expr, context)
            // (which is just equivalent to: $(context).find(expr)
            } else {
                return this.constructor( context ).find( selector );
            }

        // HANDLE: $(DOMElement)
        } else if ( selector.nodeType ) {
            this.context = this[0] = selector;
            this.length = 1;
            return this;

        // HANDLE: $(function)
        // Shortcut for document ready
        } else if ( jQuery.isFunction( selector ) ) {
            return rootjQuery.ready !== undefined ?
                rootjQuery.ready( selector ) :
                // Execute immediately if ready is not present
                selector( jQuery );
        }

        if ( selector.selector !== undefined ) {
            this.selector = selector.selector;
            this.context = selector.context;
        }

        return jQuery.makeArray( selector, this );
    };

Source Code: src/core/init.js

jQuery’s Array-like properties

An interesting feature of the jQuery is that instances have Array like properties as each instance has a length property and stores its elements in numeric indices for its elements.

Here is a sample of properties from a jQuery object:

Array-like properties from jQuery object
1
2
3
4
5
6
7
8
9
{
    0: div.box,
    1: div.box,
    2: div.box,
    3: div.box,
    4: div.box,
    length: 5
    // ...
}

jQuery’s Array-like behavior meshes well with method chaining, because for methods that return this or the instance, you now have the option to not only call another instance method but also to access selected elements.

Method Chaining and jQuery’s Array-like properties
1
2
3
4
5
6
7
8
9
10
11
12
13
// Assume we have html divs with a box class

// we can call a method after the constructor returns
$(".box").addClass("red");

// we can access an element
$(".box")[0] // get the first box

// we can chain methods to gether
$(".box").addClass("red").css("opacity", 0.7);

// add class to all boxes and get the first elemeent
$(".box").addClass("foo")[0];

jQuery also has an instance method called get that retrieves the elements matched from jQuery, using the array like properties we discussed above.

Note that this is different static jQuery.get that is used to load data from the server with a HTTP GET request.

jQuery.fn.get
1
2
3
4
5
6
7
8
9
10
11
// Get the Nth element in the matched element set OR
// Get the whole matched element set as a clean array
get: function( num ) {
    return num != null ?

        // Return just the one element from the set
        ( num < 0 ? this[ num + this.length ] : this[ num ] ) :

        // Return all the elements in a clean array
        slice.call( this );
},

Source Code: src/core.js

Constuctor Walkthroughs

Now we’ll discuss a few applications/internals of the jQuery constructor. For a more detailed view of the meat of the constructor refer to src/core/init.

First of all if nothing is passed to the constructor, the current instance is simply returned.

falsy values passed to jQuery constuctor
1
2
3
4
// HANDLE: $(""), $(null), $(undefined), $(false)
if ( !selector ) {
    return this;
}

Source Code: src/core/init.js

Next the constructor handles strings. At first the function checks if the string contains brackets and then uses a regex expression to check whether or not the string matches html or contains an id.

The regex expression is called rquickExpr.

rquickExpr
1
2
3
4
// A simple way to check for HTML strings
// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
// Strict HTML recognition (#11290: must start with <)
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,

Source src/core/init

The example I will first be discussing is a simple selection for a class.

1. Select selector

Find selector
1
    $(".box") // find all elements with the box class

Since this selector will not match the regex described above, the search will the constructor will invoke jQuery.fn.find to locate the search the document for the relevant elements

invoke jQuery.fn.find
1
2
3
4
5
// HANDLE: $(expr, $(...))
    if (...) {
    } else if ( !context || context.jquery ) {
        return ( context || rootjQuery ).find( selector );
    }

This is the function definition for jQuery.fn.find.

jQuery.fn.find
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
jQuery.fn.extend({
    find: function( selector ) {
        var i,
            len = this.length,
            ret = [],
            self = this;

        if ( typeof selector !== "string" ) {
            return this.pushStack( jQuery( selector ).filter(function() {
                for ( i = 0; i < len; i++ ) {
                    if ( jQuery.contains( self[ i ], this ) ) {
                        return true;
                    }
                }
            }) );
        }

        for ( i = 0; i < len; i++ ) {
            jQuery.find( selector, self[ i ], ret ); // invoke sizzle
        }

        // Needed because $( selector, context ) becomes $( context ).find( selector )
        ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );
        ret.selector = this.selector ? this.selector + " " + selector : selector;
        return ret;
    }
});

We pass a string to jQuery.fn.find and then the static method jQuery.find is invoked. jQuery.find is really just an alias for Sizzle, the selector engine.

The results of items found by Sizzle are stored as an array in the ret variable.

That array is then passed into to the function pushStack which merges a new instance of jQuery this.constructor with the current array of matched elements, elements.

As we discussed above, invoking the constructor with no values, will just immediately return the instance of jQuery.

jQuery.pushStack
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    // Take an array of elements and push it onto the stack
    // (returning the new matched element set)
    pushStack: function( elems ) {

        // Build a new jQuery matched element set
        var ret = jQuery.merge( this.constructor(), elems );

        // Add the old object onto the stack (as a reference)
        ret.prevObject = this;
        ret.context = this.context;

        // Return the newly-formed element set
        return ret;
    },

Another important aspect of push stack is that it keeps reference to the previous object or parent of the search. This is helpful for methods like end which return the reference to the parent element.

Sample use of jQuery.end
1
2
3
4
5
$( "ul.first" )
    .find( ".foo" )
        .css( "background-color", "red" ) // update .foo elements
    .end() // return to ul.first
// refer to http://api.jquery.com/end/

Source: jQuery.end

jQuery.merge (used in pushStack) copies over the second array into the first array. In this case, the elements returned from jQuery’s sizzle are copied over to the instance (this).

Notice that this form of merging is consistent with jQuery’s Array-like properties discussed above.

jQuery.merge
1
2
3
4
5
6
7
8
9
10
11
12
13
merge: function( first, second ) {
        var len = +second.length,
            j = 0,
            i = first.length;

        for ( ; j < len; j++ ) {
            first[ i++ ] = second[ j ];
        }

        first.length = i;

        return first;
}

Source: src/core.js

2. Build HTML

If the content matches the HTML regex describe previously, an array of DOM nodes generated from the parsed html is merged into the current instance.

Sample for constructing elements with jQuery using html
1
    $("<div></div>") // build a div element
parse html in constructor
1
2
3
4
5
jQuery.merge( this, jQuery.parseHTML(
                    match[1],
                    context && context.nodeType ? context.ownerDocument || context : document,
                    true
));

3. document.ready

$(document).ready( handler ) is a construct that is used to invoke the handler when the DOM has been constructed.

Since document is a DOM element it will simply be stored as element 0 of the instance.

handle DOMElement
1
2
3
4
5
if ( selector.nodeType ) {
    this.context = this[0] = selector;
    this.length = 1;
    return this;
}

Source: src/core/init.js

Then jQuery.fn.ready is invoked with the callback. An interesting to note is that that invoking .ready does not require any elements although the api does not recommend using ready like this: $().ready( handler ) (this is not recommened).

There is another shorthand for $(document).ready where you pass the function to the constructor.

Shorthand document.ready
1
    $( handler )

Internally jQuery stores a reference to the document called rootjQuery. If a function is passed to the constructor, then ready is called on rootjQuery.

rootjQuery
1
2
3
4
5
6
7
8
9
// A central reference to the root jQuery(document)
var rootjQuery,

// ...

// ...

// Initialize central reference
rootjQuery = jQuery( document );

Source code: src/core/init.js

Handle: $(function)
1
2
3
4
5
6
7
8
9
10
// ...
} else if ( jQuery.isFunction( selector ) ) {
    return rootjQuery.ready !== undefined ?
        rootjQuery.ready( selector ) :
        // Execute immediately if ready is not present
        selector( jQuery );

}

// ...

Source code: src/core/init.js

4. $({})

Finally if the item passed into the constuctor does not match any of these cases (and others not discussed here) the constructor simply makes an array out of the element passed in.

Example that reaches default case
1
2
    $({});
    // returns [{}]
default constructor case
1
return jQuery.makeArray( selector, this );

Source code: src/core/init. More on makeArray.

Why would use a construct like $({})? Wrapping an object around jQuery allows you to apply jQuery’s methods onto the object.

Ben Alman wrote a tiny pub-sub system using such a construct.

Tiny Pub-Sub by Ben Alman
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* jQuery Tiny Pub/Sub - v0.7 - 10/27/2011
 * http://benalman.com/
 * Copyright (c) 2011 "Cowboy" Ben Alman; Licensed MIT, GPL */

(function($) {

  var o = $({});

  $.subscribe = function() {
    o.on.apply(o, arguments);
  };

  $.unsubscribe = function() {
    o.off.apply(o, arguments);
  };

  $.publish = function() {
    o.trigger.apply(o, arguments);
  };

}(jQuery));

Source Code jquery-tiny-pubsub

Static Methods

jQuery has static methods which do not require an instance of jQuery to operate. Many of these methods are utility methods such as isArray or each.

Static Method Example: $.each
1
2
3
4
5
6
$.each([1,2,3], function(i, el){
   console.log(el);
});
// 1
// 2
// 3

An interesting thing to note is that there are static methods and instance methods in the codebase that have the same name.

Many of these instance methods also invoke the static methods within their definition.

jQuery.fn.each instance method
1
2
3
4
5
// ...
each: function( callback, args ) {
    return jQuery.each( this, callback, args ); // use static each
},
// ...

Source: jQuery.fn.each jQuery.each

jQuery.fn.find instance method
1
2
3
4
5
6
7
8
9
10
11
12
13
find: function( selector ) {
    var i,
        len = this.length,
        ret = [],
        self = this;

    // ...
    for ( i = 0; i < len; i++ ) {
        jQuery.find( selector, self[ i ], ret ); // use static jQuery.find
    }

    // ...
}

Source: jQuery.find jQuery.fn.find

Implicit Iteration

Under the hood, many of jQuery’s methods such as addClass iterate over all the elements that were selected, which makes the API very to use especially for first time coders.

Sample Use Case Implicit Iteration
1
$(".box").addClass("red"); // add red class to all elements that contain the box class

Given our understanding of static methods and jQuery’s Array-like Properties, implementing implicit iteration (automatic iteration hidden from the API level) involves iterating over the this object reference that contains all the elements.

Simple Iteration addClass
1
2
3
4
for ( ; i < len; i++ ) {
    elem = this[ i ]; // recall that each element is stored in the this object
    // ... do some class manipulation for the element
}

Source Code: src/attributes/classes.js

If you are passing a function to addClass, jQuery uses the this.fn.each to iterate over the object. This is because this.fn.each uses jQuery.each which invokes the function on each element. So in the inner line of the callback this actually refers to the individual element.

this.each iteration addClass
1
2
3
4
5
6
if ( jQuery.isFunction( value ) ) {
    return this.each(function( j ) {
        // 'this' here refers to the individual element.
        jQuery( this ).addClass( value.call( this, j, this.className ) ); // invoke function and individual method
    });
}

Source Code: src/attributes/classes.js

More details on jQuery.each

I hope this article gives you a good overview of how jQuery is built and certain aspects of the codebase. Applying these principles I hope you will be at more ease should you choose to further explore jQuery’s codebase or write more JavaScript.

Comments