the dom is a mess @ yahoo

Post on 08-Sep-2014

51.094 Views

Category:

Technology

0 Downloads

Preview:

Click to see full reader

DESCRIPTION

A talk that I gave at Yahoo, in January 2009, about the DOM.

TRANSCRIPT

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

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/

top related