Recent Changes to jQuery's Internals

51
September 12, 2009 Recent Changes to jQuery’s Internals John Resig

Transcript of Recent Changes to jQuery's Internals

September 12, 2009

Recent Changes tojQuery’s InternalsJohn Resig

No More Browser Sniffing

✤ As of jQuery 1.3

✤ jQuery.browser is now deprecated

✤ All uses of jQuery.browser have been removed from core.

✤ jQuery.support replaces it.

jQuery.support

✤ div.innerHTML = ' <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a </a><select><option>text</option></select>';

jQuery.support

✤ jQuery.support = {

! ! // IE strips leading whitespace when .innerHTML is used! ! leadingWhitespace: div.firstChild.nodeType == 3,

! ! // Make sure that tbody elements aren't automatically inserted! ! // IE will insert them into empty tables! ! tbody: !div.getElementsByTagName("tbody").length,

! ! // Make sure that link elements get serialized correctly by innerHTML! ! // This requires a wrapper element in IE! ! htmlSerialize: !!div.getElementsByTagName("link").length,

! ! // Get the style information from getAttribute! ! // (IE uses .cssText insted)! ! style: /red/.test( a.getAttribute("style") ),

! ! // Make sure that URLs aren't manipulated! ! // (IE normalizes it by default)! ! hrefNormalized: a.getAttribute("href") === "/a",

! ! // Make sure that element opacity exists! ! // (IE uses filter instead)! ! opacity: a.style.opacity === "0.5",

! ! // Verify style float existence! ! // (IE uses styleFloat instead of cssFloat)! ! cssFloat: !!a.style.cssFloat,

! ! // Will be defined later! ! scriptEval: false,! ! noCloneEvent: true,! ! boxModel: null! };

Modularity

✤ Starting in 1.3.3 code is broken up even more

✤ Core has been split into many sub-modules

✤ core.js

✤ attributes.js

✤ css.js

✤ manipulation.js

✤ traversing.js

Modularity

✤ Starting to reduce dependencies in-between files

✤ Makes it easier to dynamically load portions of the library

✤ (Mobile jQuery!)

Mobile jQuery?

✤ “Heavy” core.js

✤ ready event

✤ find() (powered by querySelectorAll)

✤ filter() (very basic functionality)

✤ $.getScript() (for loading modules)

✤ Still experimenting...

Sizzle

✤ Standalone selector engine, landed in jQuery 1.3.

✤ Built for selectors that people actually use.Selector % Used # of Uses#id 51.290% 1431.class 13.082% 365tag 6.416% 179tag.class 3.978% 111#id tag 2.151% 60tag#id 1.935% 54#id:visible 1.577% 44#id .class 1.434% 40.class .class 1.183% 33* 0.968% 27#id tag.class 0.932% 26#id:hidden 0.789% 22tag[name=value] 0.645% 18.class tag 0.573% 16[name=value] 0.538% 15tag tag 0.502% 14#id #id 0.430% 12#id tag tag 0.358% 10

Right to Left

✤ Most selector engines (including jQuery, pre-1.3) evaluate a selector left to right

✤ “#id div” - finds the element by ID “id” then finds all divs inside of it.

✤ Sizzle finds all divs then checks to see if theres an ancestor with an id of “id”.

✤ How CSS engines work in browsers.

✤ Only one query per selector, rest is filtering.

Reducing Complexity

Analyzing Performance

✤ Optimizing performance is a huge concern: Faster code = happy users!

✤ Measure execution time

✤ Loop the code a few times

✤ Measure the difference:

✤ (new Date).getTime();

Stack Profiling

✤ jQuery Stack Profiler

✤ Look for problematic methods and plugins

✤ http://ejohn.org/blog/deep-profiling-jquery-apps/

FireUnit

✤ A simple JavaScript test suite embedded in Firebug.

✤ http://fireunit.org/

FireUnit Profile Data

{ "time": 8.443, "calls": 611, "data":[ { "name":"makeArray()", "calls":1, "percent":23.58, "ownTime":1.991, "time":1.991, "avgTime":1.991, "minTime":1.991, "maxTime":1.991, "fileName":"jquery.js (line 2059)" }, // etc.]}

fireunit.getProfile();

http://ejohn.org/blog/function-call-profiling/

Complexity Analysis

✤ Analyze complexity rather than raw time

✤ jQuery Call Count Profiler (uses FireUnit)Method Calls Big-O

.addClass("test"); 542 6n

.addClass("test"); 592 6n

.removeClass("test"); 754 8n

.removeClass("test"); 610 6n

.css("color", "red"); 495 5n

.css({color: "red", border: "1px solid red"}); 887 9n

.remove(); 23772 2n+n2

.append("<p>test</p>"); 307 3n

Complexity Analysis

✤ Reducing call count helps to reduce complexity

✤ Results for 1.3.3:

Method Calls Big-O

.remove(); 298 3n

.html("<p>test</p>"); 507 5n

.empty(); 200 2n

http://ejohn.org/blog/function-call-profiling/

ECMAScript 5 Support

✤ New in 1.3.3

✤ Removal of arguments.callee

✤ Switching from ‘RegExp to ‘new RegExp’

✤ Using JSON.parse, where available.

✤ Support for json2.js is auto-baked in.

Unified Syntax

✤ Reusable inline functions and RegExp moved outside.

✤ var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g,

! rleadingWhitespace = /^\s+/,! rsingleTag = /^<(\w+)\s*\/?>$/,! rxhtmlTag = /(<(\w+)[^>]*?)\/>/g,! rselfClosing = /^(?:abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i,! rinsideTable = /^<(thead|tbody|tfoot|colg|cap)/,! rtbody = /<tbody/i,! fcloseTag = function(all, front, tag){! ! return rselfClosing.test(tag) ?! ! ! all :! ! ! front + "></" + tag + ">";! };

✤ http://docs.jquery.com/JQuery_Core_Style_Guidelines

Type Checks

String: typeof object === "string"• Number: typeof object === "number"• Boolean: typeof object === "boolean"• Object: typeof object === "object"• Function: jQuery.isFunction(object)• Array: jQuery.isArray(object)• Element: object.nodeType• null: object === null• undefined: typeof variable === "undefined" or object.prop === undefined• null or undefined: object == null

isFunction / isArray

✤ isFunction overhauled in 1.3, isArray added in 1.3.3

toString = Object.prototype.toString

isFunction: function( obj ) {

! ! return toString.call(obj) === "[object Function]";! },

! isArray: function( obj ) {! ! return toString.call(obj) === "[object Array]";! },

Core

.selector / .context

✤ Added in 1.3

✤ Predominantly used for plugins

✤ var div = jQuery(“div”);div.selector === “div”;div.context === document;

✤ jQuery.fn.update = function(){ return this.pushStack( jQuery(this.selector, this.context), “update” );};

jQuery(...) unified

✤ In 1.3.3

✤ jQuery(null), jQuery(false), jQuery(undefined) => jQuery([])

✤ jQuery() is still the same as jQuery(document) (for now)

✤ BUT jQuery(“div”) points back to a common jQuery(document) root.

✤ jQuery(“div”).prevObject === jQuery()

jQuery(“TAG”)

✤ In 1.3.3

✤ jQuery(“body”) is now a shortcut for jQuery(document.body)

✤ jQuery(“TAG”) is optimized (skips Sizzle)

Position

✤ All in 1.3.3

✤ .get(-N), .eq(-N)Access elements/jQuery sets using negative indices.

✤ .first(), .last()Shortcuts for .eq(0) and .eq(-1)

✤ .toArray()Shortcut for .get()

.data()

✤ In 1.3.3

✤ Calling .data() (no args) returns the full data object for an element

✤ jQuery(“#foo”).data(“a”, 1).data(“b”, 2).data() => { a: 1, b: 2 }

Selectors

Document Order

if ( document.documentElement.compareDocumentPosition ) {! sortOrder = function( a, b ) {! ! var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1;! ! if ( ret === 0 ) {! ! ! hasDuplicate = true;! ! }! ! return ret;! };} else if ( "sourceIndex" in document.documentElement ) {! sortOrder = function( a, b ) {! ! var ret = a.sourceIndex - b.sourceIndex;! ! if ( ret === 0 ) {! ! ! hasDuplicate = true;! ! }! ! return ret;! };} else if ( document.createRange ) {! sortOrder = function( a, b ) {! ! var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange();! ! aRange.selectNode(a);! ! aRange.collapse(true);! ! bRange.selectNode(b);! ! bRange.collapse(true);! ! var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange);! ! if ( ret === 0 ) {! ! ! hasDuplicate = true;! ! }! ! return ret;! };}

✤ As of 1.3.2 all selectors return in document order(as if you did getElementsByTagName(“*”))

:hidden / :visible

✤ Changed in 1.3.2, revised in 1.3.3

✤ Changed logic of :hidden/:visible to use .offsetWidth === 0 && .offsetHeight === 0 trick

✤ Sizzle.selectors.filters.hidden = function(elem){

! var width = elem.offsetWidth, height = elem.offsetHeight,! ! force = /^tr$/i.test( elem.nodeName ); // ticket #4512! return ( width === 0 && height === 0 && !force ) ?! ! true :! ! ! ( width !== 0 && height !== 0 && !force ) ?! ! ! ! false :! ! ! ! ! !!( jQuery.curCSS(elem, "display") === "none" ); };

ID-rooted Queries

✤ Changed in 1.3.3

✤ $(“#id div”) sped up dramatically

✤ #id is detected and used as the root of the query

✤ $(“div”, “#id”) --> $(“#id”).find(“div”)

✤ $(“#id div”) can likely never be as fast as $(“#id”).find(“div”)

✤ $(“#id”) is so hyper-optimized, it’s hard to match raw perf.

DOM Manipulation

HTML Fragments

✤ Overhauled in 1.3

✤ .append(“<li>foo</li>”) first converts HTML to a DOM fragment

✤ Fragments act as a loose collection for DOM nodes

✤ Can be used to insert many nodes with a single operation

✤ var fragment = document.createDocumentFragment();while ( node.firstChild ) fragment.appendChild( node.firstChild );document.body.appendChild( node );

HTML Fragments (redux.)

✤ In 1.3.3

✤ It turns out that using fragments makes it really easy to cache them, as well.

✤ Look for common cases that can be cached (simple, small, HTML strings that are built more than once).

✤ Also optimized $(“<some html>”), cut out some unnecessary intermediary code.

✤ jQuery.fragments[“<some html>”] = fragment;

HTML Fragments

✤ Surprising change:

✤ for ( var i = 0; i < 250; i++ ) { $(“<li>some thing</li>”).attr(“id”, “foo” + i).appendTo(“ul”);}

✤ Now faster than:

✤ for ( var i = 0; i < 250; i++ ) { $(“ul”).append(“<li id=‘foo” + i + “‘>some thing</li>”);}

$(“<div/>”)

✤ In 1.3:

✤ $(“<div/>”) was made equivalent to $(document.createElement(“div”))

✤ In 1.3.3:

✤ Logic for handling createElement moved to jQuery() (performance improved!)

append(function(){ })

✤ In 1.3.3 append, prepend, before, after, wrap, wrapAll, replace, replaceAll all take a function as an argument

✤ Compliments .attr(“title”, function(){}) (and CSS)

✤ Makes:$(“li”).each(function(){ $(this).append(“My id: “ + this.id);});

✤ Also:$(“li”).append(function(){ return “My id: “ + this.id;});

.detach()

✤ In 1.3.3

✤ Like .remove() but leaves events and data intact.

✤ detach: function( selector ) {

! ! return this.remove( selector, true );! },

DOM Traversal

.closest()

✤ Added in 1.3

✤ $(this).closest(“div”) checks if ‘this’ is a div, if not keeps going up the tree until it finds the closest one.

✤ In 1.3.3

✤ $(this).closest(“.test”, document.body)Prevent the traversal from going too far up the tree.

find() perf

✤ In 1.3.3

✤ Reduced the number of function calls - Reduced 16 calls per find() to 5.

✤ find: function( selector ) {

! ! var ret = this.pushStack( "", "find", selector ), length = 0;

! ! for ( var i = 0, l = this.length; i < l; i++ ) {! ! ! length = ret.length;! ! ! jQuery.find( selector, this[i], ret );

! ! ! if ( i > 0 ) {! ! ! ! // Make sure that the results are unique! ! ! ! for ( var n = length; n < ret.length; n++ ) {! ! ! ! ! for ( var r = 0; r < length; r++ ) {! ! ! ! ! ! if ( ret[r] === ret[n] ) {! ! ! ! ! ! ! ret.splice(n--, 1);! ! ! ! ! ! ! break;! ! ! ! ! ! }! ! ! ! ! }! ! ! ! }! ! ! }! ! }

! ! return ret;! },

find() perf

✤ Perf for $(“...”) improved, as well (hooked into rootQuery, uses less function calls)

.not() / .filter()

✤ .not(function(){}) (1.3.3)

✤ .filter(Element), .filter(function(){}) (1.3.3)

✤ Full API parity inbetween .not() and .filter()

.index()

✤ In 1.3.3

✤ $(“div”).index() - position of element relative to siblings

✤ $(“#foo”).index(“div”) - position relative to all divs

Events

Live Events

✤ Super-efficient event delegation - uses .closest(), introduced in 1.3.

✤ 1.3.3 adds context and data object support.

✤ 1.3.3 will ship once “submit”, “change”, and “focus/blur” events work in .live() (in all browsers).

.bind() `thisObject`

✤ In 1.3.3

✤ You can now bind functions enforcing this `this`

✤ var obj = { method: function(){} };$(“div”).bind( “click”, function(){ this.objProp = true;}, obj );

Dynamic Ready Event

✤ In 1.3.3

✤ document.readyState is checked to determine if the body is already loaded - if so, no need to wait for ready event.

Method Calls O(N).addClass("test"); 542 O(6n)

.addClass("test"); 592 O(6n)

.removeClass("test"); 754 O(8n)

.removeClass("test"); 610 O(6n)

.css("color", "red"); 495 O(5n)

.css({color: "red", border: "1px solid red"}); 887 O(9n)

.remove(); 23772 O(2n+n2)

.append("<p>test</p>"); 307 O(3n)

.append("<p>test</p><p>test</p><p>test</p><p>test</p><p>test</p>"); 319 O(3n)

.show(); 394 O(4n)

.hide(); 394 O(4n)

.html("<p>test</p>"); 28759 O(3n+n2)

.empty(); 28452 O(3n+n2)

.is("div"); 110

.filter("div"); 216 O(2n)

.find("div"); 1564 O(16n)

Method Calls O(N).addClass("test"); 2

.addClass("test"); 2

.removeClass("test"); 2

.removeClass("test"); 2

.css("color", "red"); 102

.css({color: "red", border: "1px solid red"}); 201 O(2n)

.remove(); 299 O(3n)

.append("<p>test</p>"); 116

.append("<p>test</p><p>test</p><p>test</p><p>test</p><p>test</p>"); 128

.show(); 394 O(4n)

.hide(); 394 O(4n)

.html("<p>test</p>"); 100

.empty(); 201 O(2n)

.is("div"); 109

.filter("div"); 216 O(2n)

.find("div"); 495 O(5n)