secrets of javascript libraries
DESCRIPTION
This is the presentation from the "Secrets of JavaScript Libraries" panel at SXSW 2008.TRANSCRIPT
Secrets ofJavaScript 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 theJavaScript 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: this.color }); }, 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) } }, ...
$(document.body).insert(new Clock().element);$(document.body).insert(new Clock({ color: "red" }).element);$(document.body).insert(new Clock({ format: "#{hour}:#{minute}" }).element);
// 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 ofCross-Browser Code
Browser Sniffing(wait, hear me out)
Conditionally evil
In order of desirability:
capabilities&
quirks
Capabilitiesare easy to sniff
Quirksare... more troublesome
if (document.evaluate) { // ...}
Object detection(for capabilities)
Distill to a booleanvar thinksCommentsAreElements = false;if ( document.createElement('!') ) { thinksCommentsAreElements = true;}
(for stuff in the gray area)
...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 contractGood faith from browser makers ➝
good faith from web authors
Secrets of QualityAssurance
0
750
1,500
1.5.01.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 ofEvents
Event handlersare tricky
Don’t store them on the element itselfcircular 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 heredata[1] = { handlers: { click: [ function(){...} ], ... }};
Then clean up on page unload
So that IE doesn’t keep themin memory in perpetuum
Fixing memory leaks
Internet Explorer 6red-headed stepchild
Don’t “prove” your code has no leaks
(view the results!)
Dripis awesome
Test page:Create a bunch of elements, assign each an event
handler, then remove each one from the page
Demonstrating the LeakNotice the stair-step effect
Plugging the leak// In Prototype, will remove all event listeners from an elementEvent.stopObserving(someElement);
Event.purgeObservers = function(element, includeParent) { Element.select(element, "*").each(Event.stopObserving); if ( includeParent ) Event.stopObserving( element );};
Redefining functionsadd “before advice” to functionsthat need to remove elements
Code sampleElement.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 fixfor 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 ofDOM 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 ofStyle
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 propertiesclientWidth, offsetWidth
The value you want is not captured by any property
Lorem ipsum dolor sit
amet.
width
clientWidth
offsetWidth
Computed stylesgetting padding & border value
The fool-proof, painful way
Take the offsetWidth,then subtract computed padding & border
Code exampleElement.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
CONTENTEXPIRATION
Concatenation
GZIP4-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)