You are on page 1of 78

Secrets of

JavaScript Libraries
(Left to Right)
Sam Stephenson (Prototype)
Alex Russell (Dojo)
Thomas Fuchs (Script.aculo.us)
Andrew Dupont (Prototype)
John Resig (jQuery)
What to Cover
✦ Topics:
✦ JavaScript Language
✦ Cross-Browser Code
✦ Events
✦ DOM Traversal
✦ Style
✦ Animations
✦ Distribution
✦ HTML Insertion
Secrets of the
JavaScript Language
// Set up a class and create an element

var Clock = Class.create({


initialize: function() {
this.createElement();
},

createElement: function() {
this.element = new Element("div");
}
});
// Display the time

var Clock = Class.create({


initialize: function() {
this.createElement();
},

createElement: function() {
this.element = new Element("div");

var date = new Date();


this.element.update(
date.getHours() + ":" +
date.getMinutes().toPaddedString(2) + "." +
date.getSeconds().toPaddedString(2)
);
}
});

$(document.body).insert(new Clock().element);
// Add the timer

var Clock = Class.create({


initialize: function() {
this.createElement();
this.createTimer();
},

createElement: function() {
this.element = new Element("div");
},

updateElement: function() {
var date = new Date();
this.element.update(
date.getHours() + ":" +
date.getMinutes().toPaddedString(2) + "." +
date.getSeconds().toPaddedString(2)
);
},

createTimer: function() {
window.setInterval(500, this.updateElement.bind(this));
}
});
// Add some options

var Clock = Class.create({


color: "black",
format: "#{hour}:#{minute}.#{second}",

initialize: function(options) {
Object.extend(this, options);
this.createElement();
this.createTimer();
},

createElement: function() {
this.element = new Element("div");
this.element.setStyle({ color: Clock().element);
$(document.body).insert(new this.color });
}, $(document.body).insert(new Clock({ color: "red" }).element);
$(document.body).insert(new Clock({ format: "#{hour}:#{minute}" }).element);
updateElement: function() {
this.element.update(this.format.interpolate(this.getTime()));
},

getTime: function() {
var date = new Date();
return {
hour: date.getHours(),
minute: date.getMinutes().toPaddedString(2),
second: date.getSeconds().toPaddedString(2)
}
}, ...
// Use #toElement

var Clock = Class.create({


...

toElement: function() {
return this.element;
}
});

$(document.body).insert(new Clock());
$(document.body).down("div.clock > div").replace(new Clock());
$(document.body).down("div.clock").update(new Clock());
// Subclass it

var AmPmClock = Class.create(Clock, {


format: "#{hour}:#{minute}:#{second} #{ampm}",

getTime: function($super) {
var time = $super();

time.ampm = time.hour < 12 ? "am" : "pm";

if (time.hour == 0) {
time.hour = 12;
} else if (time.hour > 12) {
time.hour -= 12;
}

return time;
}
});

$(document.body).insert(new AmPmClock());
// Or monkeypatch it

Object.extend(Clock.prototype, {
format: "#{hour}:#{minute}:#{second} #{ampm}",

getTime: Clock.prototype.getTime.wrap(function(original) {
var time = original();

time.ampm = time.hour < 12 ? "am" : "pm";

if (time.hour == 0) {
time.hour = 12;
} else if (time.hour > 12) {
time.hour -= 12;
}

return time;
}
});

$(document.body).insert(new Clock());
Secrets of
Cross-Browser Code
Browser Sniffing
(wait, hear me out)
Conditionally evil
In order of desirability:
capabilities
&
quirks
Capabilities
are easy to sniff
Quirks
are... more troublesome
Object detection
(for capabilities)

if (document.evaluate) {
// ...
}
Distill to a boolean
(for stuff in the gray area)

var thinksCommentsAreElements = false;


if ( document.createElement('!') ) {
thinksCommentsAreElements = true;
}
...then sniff
(for outright bugs/quirks)

if (Prototype.Browser.IE) {
element.style.filter =
"alpha(opacity=50);";
}
Try to do the right thing,
but don’t stand on principle
The social contract
Good faith from browser makers ➝
good faith from web authors
Secrets
of Quality
Assurance
1,500

750

0
1.5.0
1.5.1
1.6.0.2
Debugging
✦ Localize & Reproduce
✦ Find the smallest possible code that
generates the problem
✦ Easy test-case generation
✦ ./gen.sh 1245 ajax
./gen.sh 1246 dom
✦ Simple logging, all browsers:
document.getElementById(“output”)
.innerHTML += “<br>” + msg;
Secrets of
Events
Event handlers
are tricky
Don’t store them on the
element itself
circular references = sadness
Build a global hashtable
(like jQuery does it)
Element Data Store
✦ Each element gets a unique ID
(bound w/ a unique property)
elem.jQuery12345 = 1;
✦ Points back to large data structure:
data[1] = { ... all element data here ... };
✦ Data (like event handlers) are stored here
data[1] = {
handlers: { click: [ function(){...} ], ... }
};
Then clean up
on page unload
So that IE doesn’t keep them
in memory in perpetuum
Fixing memory leaks
Internet Explorer 6
red-headed stepchild
Don’t “prove” your code
has no leaks
(view the results!)
Drip
is awesome
Test page:
Create a bunch of elements, assign each an event
handler, then remove each one from the page
Demonstrating the Leak
Notice the stair-step effect
Plugging the leak
// In Prototype, will remove all event listeners from an element
Event.stopObserving(someElement);

Event.purgeObservers = function(element, includeParent) {


Element.select(element, "*").each(Event.stopObserving);
if ( includeParent ) Event.stopObserving( element );
};
Redefining functions
add “before advice” to functions
that need to remove elements
Code sample
Element.Methods.update = Element.Methods.update.wrap(
function(proceed, element, contents) {
Event.purgeObservers(element);
return proceed(element, contents);
}
);

Element.Methods.replace = Element.Methods.replace.wrap(
function(proceed, element, contents) {
Event.purgeObservers(element, true);
return proceed(element, contents);
}
);

Element.Methods.remove = Element.Methods.remove.wrap(
function(proceed, element) {
Event.purgeObservers(element, true);
return proceed(element);
}
);

Element.addMethods();
Drop-in fix
for Prototype 1.6
Custom Events
• Everything is event based if you squint
• DOM is a good foundation
• Terrible for stitching together non-DOM
components and code
• Composition == good, inheritance == bad
• Custom events let us join loosely
• When to use them? Pitfalls?
Custom Events (contd.)
// in Dojo:
dojo.subscribe(“/foo”, function(e, arg){ ... });
dojo.publish(“/foo”, [{ data: “thinger”}, “second arg”]);

// in Prototype:
document.observe(“event:foo”, function(e){ ... });
$(“nodeId”).fire(“event:foo”, { data: “thinger” });

// in jQuery:
$(document).bind(“foo”, function(e, data, arg){ ... });
$(document).trigger(“foo”, [{ data: “thinger”}, “second”]);
Secrets of
DOM Traversal
Selector Internals

• Optimized DOM
• Top-down vs. bottom-up
• Caching + winnowing
• XPath
• Native, aka: querySelectorAll()
Selector Internals
• Tradeoffs: size vs. speed vs. complexity
• Prototype: XPath when possible, else DOM
• Good perf, lower complexity, hackable
• Dojo: all 3 methods, picks best available
• Best perf, biggest, highest complexity
• JQuery: DOM
• Good perf, small size, extensiblity vs.
forward-compat tradeoff
Secrets of
Style
Computed Style
✦ IE way vs. Everyone else
✦ IE returns “actual value”
✦ Everyone else returns “pixel value”
✦ font-size: 2em;
IE: “2em”
Other: 24
✦ Need to convert to common base

✦ Performance: Very costly, generally avoided


wherever possible
Pixel Values in IE
✦ if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) {

// Remember the original values

var style = elem.style.left, runtimeStyle = elem.runtimeStyle.left;


// Put in the new values to get a computed value out

elem.runtimeStyle.left = elem.currentStyle.left;

elem.style.left = ret || 0;

ret = elem.style.pixelLeft + “px”;


// Revert the changed values

elem.style.left = style;

elem.runtimeStyle.left = runtimeStyle;
}

✦ From Dean Edwards:


http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
Computed Style
✦ Safari 2 & 3:
Giant mess for display: none elements
✦ Safari 2
✦ getComputedStyle() returns undefined
✦ Safari 3
✦ Always return empty strings for value
✦ Much harder to detect
var ret = document.defaultView.getComputedStyle( elem, null );
return !ret || ret.getPropertyValue(”color”) == “”;
Finding dimensions
Dimensions of what?
content box, border box, margin box...
DHTML properties
clientWidth, offsetWidth
The value you want is not captured by any property

Lorem ipsum dolor sit


amet.

width

clientWidth

offsetWidth
Computed styles
getting padding & border value
The fool-proof,
painful way
Take the offsetWidth,
then subtract computed padding & border
Code example
Element.getCSSWidth = function(element) {
element = $(element);
return element.offsetWidth -
parseFloat(element.getStyle("borderLeft")) -
parseFloat(element.getStyle("paddingLeft")) -
parseFloat(element.getStyle("paddingRight")) -
parseFloat(element.getStyle("borderRight"));
};
Secrets
of Animation
Old-School
“Effects”
setInterval
Events-based
Secrets
ofJavaScript
Deployment
CONTENT
EXPIRATION
Concatenation
GZIP
4-5x smaller
Packaging
• Dev vs. deployment constraints
• No library a single file, but all ship that way
• # of requests largest constraint
• Sync vs. async
• Static resource servers + CDNs
• Dependency management matters!
• Runtime vs. deployment
Packaging
// in Dojo:
dojo.provide(“foo.bar.Baz”);
dojo.require(“dojox.dtl”);

// in GWT:
package com.foo.bar;
import com.foo.bar.Blah;

// in JSAN:
JSAN.use(“foo.bar.Blah”);
// exports handled by build tools
Packaging

• The build-process divide


• Library support vs. server concat/shrink
• Can we strip “dead” code?
• Social artifacts of packaging systems
HTML Insertion
✦ $(“div”).append(“<b>foo</b>”);
✦ Convert HTML String
✦ innerHTML
✦ Range: .createContextualFragment()

✦ Must purify first:


✦ Fix <table>s for IE (<tbody> wrap)
✦ Handle <option>s (contain in <select>)
HTML Insertion
✦ // Fix “XHTML”-style tags in all browsers
elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){


return tag.match(/^(abbr|br|col|img|input|link|meta|param|
hr|area|embed)$/i) ?



all :



front + “></” + tag + “>”;
});

✦ $(“<abbr/>”).html(“hello!”).appendTo(“#foo”);
Script Execution
✦ Execute Script in Global Scope

✦ var head = document.getElementsByTagName(”head”)[0] ||


document.documentElement,

script = document.createElement(”script”);

script.type = “text/javascript”;
if ( jQuery.browser.msie )

script.text = data;
else

script.appendChild( document.createTextNode( data ) );

head.appendChild( script );
head.removeChild( script );
Questions
✦ Panelists:
✦ John Resig (ejohn.org)
✦ Sam Stephenson (conio.net)
✦ Alex Russell (alex.dojotoolkit.org)
✦ Thomas Fuchs (script.aculo.us/thomas)
✦ Andrew Dupont (andrewdupont.net)

✦ Frameworks:
✦ Prototype (prototypejs.org)
✦ jQuery (jquery.com)
✦ Dojo (dojotoolkit.org)
✦ Script.aculo.us (script.aculo.us)