The DOM is a Mess @ Yahoo
-
Upload
jeresig -
Category
Technology
-
view
51.094 -
download
0
description
Transcript of The DOM is a Mess @ Yahoo
The DOM is a MessJohn Resig
http://ejohn.org/ - http://twitter.com/jeresig/
A Tour of the DOM✦ A messy DOM✦ Writing Cross-Browser Code✦ Common Features
✦ CSS Selector Engine✦ DOM Modification✦ Events
Messy✦ Nearly every DOM method is broken in
some way, in some browser.✦ Some old:
✦ getElementById✦ getElementsByTagName
✦ Some new:✦ getElementsByClassName✦ querySelectorAll
getElementById✦ Likely the most commonly used DOM
method✦ A couple weird bits:
✦ IE and older versions of Opera returning elements with a name == id
✦ Does not easily work in XML documents
getElementsByTagName✦ Likely tied for most-commonly-used
DOM method✦ Riddled with bugs in IE:
✦ “*” returns no elements in IE 5.5✦ “*” returns no elements on <object>
elements in IE 7✦ .length gets overwritten in IE if an
element with an ID=”length” is found
getElementsByClassName✦ Landed in Firefox 3, Safari 3, Opera 9.6✦ A few knotty issues:
✦ HTMLElement.prototype .getElementsByClassNamecouldn’t be overwritten in Firefox
✦ Opera doesn’t match a second-specified class (e.g. class=”a b”, b isn’t found)
querySelectorAll✦ Find DOM elements using CSS selectors✦ In Firefox 3.1, Safari 3.1, Opera 10, IE 8✦ Birthing pains:
✦ Doesn’t exist in quirks mode, in IE 8✦ Safari 3.1 had memory out of bounds
problems✦ Safari 3.2 can’t match uppercase
characters in quirks mode✦ #id doesn’t match in XML documents
Moral✦ If there’s a DOM method, there’s probably
a problem with it somewhere, in some capacity.
Cross-Browser Code
Strategies✦ Pick your browsers✦ Know your enemies✦ Write your code
Cost / Benefit
IE 7 IE 6 FF 3 Safari 3 Opera 9.5
Cost Benefit
Draw a line in the sand.
Graded Support
Yahoo Browser Compatibility
Browser Support GridIE Firefox Safari Opera Chrome
Previous 6.0 2.0 3.0 9.5
Current 7.0 3.0 3.2 9.6 Current
Next 8.0 3.1 4.0 10.0
jQuery Browser Support
Browser Support GridIE Firefox Safari Opera Chrome
Previous 6.0 2.0 3.0 9.5
Current 7.0 3.0 3.2 9.6 Current
Next 8.0 3.1 4.0 10.0
jQuery 1.3 Browser Support
Know Your Enemies
JavaScript CodeMissing Features
Regressions
Browser Bugs
ExternalCode, Markup
Bug Fixes
Points of Concern for JavaScript Code
Know Your Enemies
JavaScript CodeMissing Features
Regressions
Browser Bugs
ExternalCode, Markup
Bug Fixes
Points of Concern for JavaScript Code
Browser Bugs✦ Generally your primary concern✦ Your defense is a good test suite
✦ Prevent library regressions✦ Analyze upcoming browser releases
✦ Your offense is feature simulation✦ What is a bug?
✦ Is unspecified, undocumented, behavior capable of being buggy?
Test, Test, Test
1446 Tests, 9 browsers
Know Your Enemies
JavaScript CodeMissing Features
Regressions
Browser Bugs
ExternalCode, Markup
Bug Fixes
Points of Concern for JavaScript Code
External Code✦ Making your code resistant to any
environment✦ Found through trial and error✦ Integrate into your test suite
✦ Other libraries✦ Strange code uses
Environment Testing✦ 100% Passing:
✦ Standards Mode✦ Quirks Mode✦ Inline with Prototype + Scriptaculous✦ Inline with MooTools
✦ Work in Progress:✦ XHTML w/ correct mimetype✦ With Object.prototype✦ In XUL (Firefox Extensions)✦ In Rhino (with Env.js)
Object.prototype Object.prototype.otherKey = "otherValue"; var obj = { key: "value" }; for ( var prop in object ) { if ( object.hasOwnProperty( prop ) ) { assert( prop, "key", "There should only be one iterated property." ); } }
Greedy IDs <form id="form"> <input type="text" id="length"/> <input type="submit" id="submit"/> </form>
document.getElementsByTagName("input").length
Order of Stylesheets✦ Putting stylesheets before code guarantees
that they’ll load before the code runs.✦ Putting them after can create an
indeterminate situation.
Pollution✦ Make sure your code doesn’t break
outside code✦ Use strict code namespacing✦ Don’t extend outside objects, elements
✦ Bad:✦ Introducing global variables✦ Extending native objects (Array, Object)✦ Extending DOM natives
Pollution
http://mankz.com/code/GlobalCheck.htm
Know Your Enemies
JavaScript CodeMissing Features
Regressions
Browser Bugs
ExternalCode, Markup
Bug Fixes
Points of Concern for JavaScript Code
Missing Features✦ Typically older browsers missing specific
features✦ Optimal solution is to gracefully
degrade✦ Fall back to a simplified page
✦ Can’t make assumptions about browsers that you can’t support✦ If it’s impossible to test them, you must
provide a graceful fallback✦ Object detection works well here.
Object Detection✦ Check to see if an object or property
exists✦ Useful for detecting an APIs existence✦ Doesn’t test the compatibility of an API
✦ Bugs can still exist - need to test those separately with feature simulation
Event Binding function attachEvent( elem, type, handle ) { // bind event using proper DOM means if ( elem.addEventListener ) elem.addEventListener(type, handle, false); // use the Internet Explorer API else if ( elem.attachEvent ) elem.attachEvent("on" + type, handle); }
Fallback Detection if ( typeof document !== "undefined" && (document.addEventListener || document.attachEvent) && document.getElementsByTagName && document.getElementById ) { // We have enough of an API to // work with to build our application } else { // Provide Fallback }
Fallback✦ Figure out a way to reduce the
experience✦ Opt to not execute any JavaScript
✦ Guarantee no partial API✦ (e.g. DOM traversal, but no Events)
✦ Redirect to another page, or just work unobtrusively
✦ Working on a ready() fallback for jQuery
Know Your Enemies
JavaScript CodeMissing Features
Regressions
Browser Bugs
ExternalCode, Markup
Bug Fixes
Points of Concern for JavaScript Code
Bug Fixes✦ Don’t make assumptions about browser
bugs.✦ Assuming that a browser will always
have a bug is foolhardy✦ You will become susceptible to fixes✦ Browsers will become less inclined to fix
bugs✦ Look to standards to make decisions about
what are bugs
Failed Bug Fix in FF 3 // Shouldn't work var node = documentA.createElement("div"); documentB.documentElement.appendChild( node ); // Proper way var node = documentA.createElement("div"); documentB.adoptNode( node ); documentB.documentElement.appendChild( node );
Feature Simulation✦ More advanced than object detection✦ Make sure an API works as advertised✦ Able to capture bug fixes gracefully
Verify API // Run once, at the beginning of the program var ELEMENTS_ONLY = (function(){ var div = document.createElement("div"); div.appendChild( document.createComment("test" ) ); return div.getElementsByTagName("*").length === 0; })(); // Later on: var all = document.getElementsByTagName("*"); if ( ELEMENTS_ONLY ) { for ( var i = 0; i < all.length; i++ ) { action( all[i] ); } } else { for ( var i = 0; i < all.length; i++ ) { if ( all[i].nodeType === 1 ) { action( all[i] ); } } }
Figure Out Naming <div id="test" style="color:red;"></div> <div id="test2"></div> <script> // Perform the initial attribute check var STYLE_NAME = (function(){ var div = document.createElement("div"); div.style.color = "red"; if ( div.getAttribute("style") ) return "style"; if ( div.getAttribute("cssText") ) return "cssText"; })(); // Later on: window.onload = function(){ document.getElementsById("test2").setAttribute( STYLE_NAME, document.getElementById("test").getAttribute( STYLE_NAME ) ); }; </script>
Know Your Enemies
JavaScript CodeMissing Features
Regressions
Browser Bugs
ExternalCode, Markup
Bug Fixes
Points of Concern for JavaScript Code
Regressions✦ Removing or changing unspecified APIs✦ Object detection helps here✦ Monitor upcoming browser releases
✦ All vendors provide access to beta releases
✦ Diligence!✦ Example: IE 7 introduced
XMLHttpRequest with file:// bug✦ Test Suite Integration
Object Failover function attachEvent( elem, type, handle ) { // bind event using proper DOM means if ( elem.addEventListener ) elem.addEventListener(type, handle, false); // use the Internet Explorer API else if ( elem.attachEvent ) elem.attachEvent("on" + type, handle); }
Safe Cross-Browser Fixes✦ The easiest form of fix✦ Unifies an API across browsers✦ Implementation is painless
Unify Dimensions // ignore negative width and height values if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) value = undefined;
Prevent Breakage if ( name == "type" && elem.nodeName.toLowerCase() == "input" && elem.parentNode ) throw "type attribute can't be changed";
Untestable Problems✦ Has an event handler been bound?✦ Will an event fire?✦ Do CSS properties like color or opacity
actually affect the display?✦ Problems that cause a browser crash.✦ Problems that cause an incongruous API.
Impractical to Test✦ Performance-related issues✦ Determining if Ajax requests will work
Battle of Assumptions✦ Cross-browser development is all about
reducing the number of assumptions✦ No assumptions indicates perfect code
✦ Unfortunately that’s an unobtainable goal
✦ Prohibitively expensive to write✦ Have to draw a line at some point
DOM Traversal✦ Many methods of DOM traversal✦ One unanimous solution:
✦ CSS Selector Engine
Traditional DOM✦ getElementsByTagName✦ getElementById✦ getElementsByClassName
✦ in FF3, Safari 3, Opera 9.6✦ .children
✦ only returns elements (in all, and FF 3.1)✦ getElementsByName✦ .all[id]
✦ Match multiple elements by ID
Top-Down CSS Selector✦ Traditional style of traversal
✦ Used by all major libraries✦ Work from left-to-right✦ “div p”
✦ Find all divs, find paragraphs inside✦ Requires a lot of result merging✦ And removal of duplicates
function find(selector, root){ root = root || document;
var parts = selector.split(" "), query = parts[0], rest = parts.slice(1).join(" "), elems = root.getElementsByTagName( query ), results = [];
for ( var i = 0; i < elems.length; i++ ) { if ( rest ) { results = results.concat( find(rest, elems[i]) ); } else { results.push( elems[i] ); } }
return results; }
(function(){ var run = 0;
this.unique = function( array ) { var ret = [];
run++;
for ( var i = 0, length = array.length; i < length; i++ ) { var elem = array[ i ];
if ( elem.uniqueID !== run ) { elem.uniqueID = run; ret.push( array[ i ] ); } }
return ret; }; })();
Bottom-Up✦ Work from right-to-left
✦ (How CSS Engines work in browsers.)✦ “div p”
✦ Find all paragraphs, see if they have a div ancestor, etc.
✦ Fast for specific queries✦ “div #foo”
✦ Deep queries get slow (in comparison)✦ “#foo p”
Bottom-Up✦ Some nice features:
✦ Only one DOM query✦ No merge/unique required (except for “div, span”)
✦ Everything is a process of filtering
function find(selector, root){ root = root || document;
var parts = selector.split(" "), query = parts[parts.length - 1], rest = parts.slice(0,-1).join("").toUpperCase(), elems = root.getElementsByTagName( query ), results = [];
for ( var i = 0; i < elems.length; i++ ) { if ( rest ) { var parent = elems[i].parentNode; while ( parent && parent.nodeName != rest ) { parent = parent.parentNode; }
if ( parent ) { results.push( elems[i] ); } } else { results.push( elems[i] ); } }
return results; }
CSS to XPath✦ Browsers provide XPath functionality✦ Collect elements from a document✦ Works in all browsers
✦ In IE it only works on HTML documents
✦ Fast for a number of selectors (.class, “div div div”)✦ Slow for some others: #id
✦ Currently used by Dojo and Prototype
if ( typeof document.evaluate === "function" ) { function getElementsByXPath(expression, parentElement) { var results = []; var query = document.evaluate(expression, parentElement || document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (var i = 0, length = query.snapshotLength; i < length; i++) results.push(query.snapshotItem(i)); return results; } }
Goal CSS 3 XPath
All Elements * //*
All P Elements p //p
All Child Elements p > * //p/*
Element By ID #foo //*[@id='foo']
Element By Class .foo //*[contains(concat(" ", @class, " ")," foo ")]
Element With Attribute *[title] //*[@title]
First Child of All P p > *:first-child //p/*[0]
All P with an A descendant Not possible //p[a]
Next Element p + * //p/following-sibling::*[0]
querySelectorAll✦ The Selectors API spec from the W3C✦ Two methods:
✦ querySelector (first element)✦ querySelectorAll (all elements)
✦ Works on:✦ document✦ elements✦ DocumentFragments
✦ Implemented in:✦ Firefox 3.1, Safari 3, Opera 10, IE 8
<div id="test"> <b>Hello</b>, I'm a ninja! </div> <div id="test2"></div> <script> window.onload = function(){ var divs = document.querySelectorAll("body > div"); assert( divs.length === 2, "Two divs found using a CSS selector." );
var b = document.getElementById("test").querySelector("b:only-child"); assert( b, "The bold element was found relative to another element." ); }; </script>
<div id="test"> <b>Hello</b>, I'm a ninja! </div> <script> window.onload = function(){ var b = document.getElementById("test").querySelector("div b"); assert( b, "Only the last part of the selector matters." ); }; </script>
DOM Modification✦ Injecting HTML✦ Removing Elements
Injecting HTML✦ HTML 5:
insertAdjacentHTML✦ Already in IE, dicey support, at best✦ What can we use instead?
✦ We must generate our own HTML injection
✦ Use innerHTML to generate a DOM
function getNodes(htmlString){ var map = { "<td": [3, "<table><tbody><tr>", "</tr></tbody></table>"], "<option": [1, "<select multiple='multiple'>", "</select>"] // a full list of all element fixes };
var name = htmlString.match(/<\w+/), node = name ? map[ name[0] ] || [0, "", ""];
var div = document.createElement("div"); div.innerHTML = node[1] + htmlString + node[2];
while ( node[0]-- ) div = div.lastChild;
return div.childNodes; }
assert( getNodes("<td>test</td><td>test2</td>").length === 2, "Get two nodes back from the method." ); assert( getNodes("<td>test</td>").nodeName === "TD", "Verify that we're getting the right node." );
Element Mappings✦ option and optgroup need to be contained in a
<select multiple="multiple">...</select>✦ legend need to be contained in a
<fieldset>...</fieldset>✦ thead, tbody, tfoot, colgroup, and caption need to be
contained in a <table>...</table>✦ tr need to be in a
<table><thead>...</thead></table>, <table><tbody>...</tbody></table>, or a <table><tfoot>...</tfoot></table>
✦ td and th need to be in a<table><tbody><tr>...</tr></tbody></table>
✦ col in a<table><tbody></tbody><colgroup>...</colgroup></table>
✦ link and script need to be in adiv<div>...</div>
DocumentFragment✦ Fragments can collect nodes✦ Can be appended or cloned in bulk✦ Super-fast (2-3x faster than normal)
function insert(elems, args, callback){ if ( elems.length ) { var doc = elems[0].ownerDocument || elems[0], fragment = doc.createDocumentFragment(), scripts = getNodes( args, doc, fragment ), first = fragment.firstChild;
if ( first ) { for ( var i = 0; elems[i]; i++ ) { callback.call( root(elems[i], first), i > 0 ? fragment.cloneNode(true) : fragment ); } } } }
var divs = document.getElementsByTagName("div");
insert(divs, ["Name:"], function(fragment){ this.appendChild( fragment ); });
insert(divs, ["First Last"], function(fragment){ this.parentNode.insertBefore( fragment, this ); });
Inline Script Execution✦ .append(“<script>var foo = 5;</script>”);✦ Must execute scripts globally✦ window.execScript() (for IE)✦ eval.call( window, “var foo = 5;” );✦ Cross-browser way is to build a script
element then inject it✦ Executes globally
function globalEval( data ) { data = data.replace(/^\s+|\s+$/g, "");
if ( data ) { var head = document.getElementsByTagName("head")[0] || document.documentElement, script = document.createElement("script");
script.type = "text/javascript"; script.text = data;
head.insertBefore( script, head.firstChild ); head.removeChild( script ); } }
Removing Elements✦ Have to clean up bound events
✦ IE memory leaks✦ Easy to do if it’s managed.
Events✦ Three big problems with Events:
✦ Memory Leaks (in IE)✦ Maintaining ‘this’ (in IE)✦ Fixing event object inconsistencies
Leaks✦ Internet Explorer 6 leaks horribly
✦ Other IEs still leak, not so badly✦ Attaching functions (that have a closure to
another node) as properties✦ Makes a leak:
elem.test = function(){ anotherElem.className = “foo”;};
‘this’✦ Users like having their ‘this’ refer to the
target element✦ Not the case in IE✦ What’s the solution?
Single Handler✦ Bind a single handler to an event✦ Call each bound function individually✦ Can fix ‘this’ and event object✦ How do we store the bound functions in a
way that won’t leak?
Central Data Store✦ Store all bound event handlers in a central
object✦ Link elements to handlers✦ Keep good separation✦ One data store per library instance✦ Easy to manipulate later
✦ Trigger individual handlers✦ Easy to remove again, later
Central Data StoreElement
Element
Element
Element
Element
#43
#67
#22
Data
Data
Events
Events
function click(){}function mouseover(){}
function click(){}
function click(){}
Data Store
Multiple StoresElement
Element
Element
Element
Element
#43
#67
#22
Library A
Library B
#37
Unique Element ID✦ The structure must hook to an element✦ Elements don’t have unique IDs✦ Must generate them and manage them✦ jQuery.data( elem, “events” );✦ Unique attribute name:
✦ elem.jQuery123456789 = 45;✦ Prevents collisions with other libraries
✦ We can store all sorts of data in here
Questions?✦ http://ejohn.org/✦ http://twitter.com/jeresig/