I was curious how jQuery created it’s chaining API, wrapping up the matched DOM nodes in the return object. It’s a nice pattern that would be useful to use in other scripts.

When called, the jQuery object returns an object built using the constructor invocation pattern. The jQuery methods that operate on the matched node set are exposed via the constructors prototype object, making them available through the prototype chain. The setup code also stores a reference to the prototype object in jQuery.fn, which makes it easy to add new methods later via jQuery.prototype or jQuery.fn, which refer to the same thing.

The jQuery constructor pattern

Looking at the source code, and stripping out everything but the skeleton, we are left with:

(function( window, undefined ) {

    var jQuery = (function() {

        var jQuery = function( selector, context ) {
                return new jQuery.fn.init( selector, context );
            };

        jQuery.fn = jQuery.prototype = {
            init: function( selector, context ) {
                // ...
                return this;
            }
            // jQuery API methods
        }

        jQuery.fn.init.prototype = jQuery.fn;

        return (window.jQuery = window.$ = jQuery);

    })();

})(window);

There are several layers here, including multiple reuses of the jQuery variable name within different scopes, and various prototype reference assignments.

Here’s a breakdown of the skeleton:

(function( window, undefined ) {
    // ...
})(window);

The first thing we have is a wrapper function that creates a function scope around the contained code. This means that all the variables and functions declared inside this scope will not pollute the global scope, unless the variables are declared without using ‘var’ or make themselves globally available using window (eg. window.myvar, window[‘myvar’]).

A reference to window is passed in from the outer scope, and undefined is declared but not assigned to, which in effect initialises it to undefined. Passing in these variables provides some performance efficiencies with scope chain lookups. It also helps avoid the fact that undefined can be reassigned (just like they’re doing here), so it forces it to be initialised with the actual value undefined, and not whatever may have been assigned to it outside of the library code.

var jQuery = (function() {

    var jQuery = function( selector, context ) {
            // ...
        };

    // ...

    return (window.jQuery = window.$ = jQuery);

})();

Next is another self invoking anonymous function which returns and stores the jQuery function. This outer layer was added to allow the jQuery source code to be broken into discrete modules behind the scenes to benefit ease of development. As a last step, this inner jQuery function is exposed to global scope via window.jQuery and window.$, as well as being returned to the outer jQuery variable for use by other modules in the jQuery library. In the actual jQuery source, there are additional checks and steps that provide safety against variable name collisions and the $.noConflict() mode, but I’ve removed that here to keep things focussed on the constructor logic.

return new jQuery.fn.init( selector, context );

Inside the inner jQuery function, there is the call to the jQuery constructor function. This is the jQuery functionality we’re interested. The inner function is the one that fires when the jQuery function is called in normal usage eg. $(‘p’), jQuery(‘p’).

jQuery.fn = jQuery.prototype = {
    init: function( selector, context ) {
        // Handle different use cases such as passing in selector, node set, function etc
        // When selecting / wrapping DOM nodes, the following actions are taken:
        // Get some DOM elements and add them to this[0], this[1]... this[n]
        // Set this.length to the total number of elements found
        // Store context and selector information in this.context and this.selector
        return this;
    }
    // jQuery API methods
}

// Give the init function the jQuery prototype for later instantiation
jQuery.fn.init.prototype = jQuery.fn;

The init constructor function is overloaded with lots of different cases that can be utilised, such as passing in a selector, html, DOM reference, a function etc. In the case of being passed a selector, the init function will find the DOM elements, add them to the ‘this’ parameter (which represents the instantiated jQuery object) as numerically indexed properties and store the total number of nodes found in a length property. This is where jQuery’s “array like” nature comes from, even though it’s returning an object, it creates numerically addressable entries that can be easily iterated over using the length property and a for loop.

jQuery.prototype contains the methods and properties that make up the jQuery API. A reference to this prototype object, is stored in jQuery.fn.

And finally, since the init constructor function exists as a method in the jQuery prototype object, jQuery.prototype (via jQuery.fn) is assigned as it’s prototype so that calling return new jQuery.fn.init( selector, context ); returns an object whose prototype is jQuery.prototype. This does look a little cyclic on the surface, but essentially we have a constructer function jQuery.prototype.init, that has as it’s prototype, jQuery.prototype.

Implementing our own chaining API

To simplify things, if we remove some of the layers, here’s how would we can implement the skeleton of the jQuery constructor function to provide a simple chaining API.

var myQuery, $;

(function() {

    myQuery = $ = function(selector) {
        return new MyQuery(selector);
    };

    var MyQuery = function(selector) {
        // Lets make a really simplistic selector implementation for demo purposes
        var nodes = document.getElementsByTagName(selector);
        for (var i = 0; i < nodes.length; i++) {
            this[i] = nodes[i];
        }
        this.length = nodes.length;
        return this;
    };

    // Expose the prototype object via myQuery.fn so methods can be added later
    myQuery.fn = MyQuery.prototype = {
        // API Methods
        hide: function() {
            for (var i = 0; i < this.length; i++) {
                this[i].style.display = 'none';
            }
            return this;
        },
        remove: function() {
            for (var i = 0; i < this.length; i++) {
                this[i].parentNode.removeChild(this[i]);
            }
            return this;
        }
        // More methods here, each using 'return this', to enable chaining
    };

}());

// Example usage
$('p').hide().remove();

// Add a plugin
$.fn.red = function() {
    for (var i = 0; i < this.length; i++) {
        this[i].style.color = 'red';
    }
    return this;
}

$('li').red();
posted on 2011-09-17 22:00  fyen  阅读(823)  评论(0编辑  收藏  举报