Sunday, August 8, 2010

HTML Collections are LIVE!

  var allDivs = document.getElementsByTagName('div');
  for(var i = 0; i < allDivs.length; i++) {
     document.body.appendChild(document.createElement('div'));
  }

Do you see anything wrong in this snippet of code? You would expect it to create one additional div for every div element in the DOM, so it should just double the number of div nodes in the document, right? Well, not really! This is actually an infinite loop. Why? Because the loop's exit condition is never met, as allDivs.length increases by one with every iteration.

As defined in the DOM standard, HTML collections are "assumed to be live", meaning that they are automatically updated when the underlying document is updated.

So, here is the solution: Store the collection in a local variable, cache the length outside the loop and then use a local variable inside the loop for elements that are accessed more than once. e.g

  var coll = document.getElementsByTagName('div');
  var len = coll.length;
  for (var count = 0; count < len; count++) {
     // if needed cache in here the elements, to work with
     var el = coll[count];
     // do whatever
  }
This will do the magic and solve your problem!

Let's take it one step further. What if you want to touch every element you retrieve from this collection in a way that modifies the live collection. A common example is to replace a class from a collection of elements. e.g

  var allDivs = document.getElementsByClassName('class1');
  for (var i = 0; i < allDivs.length; i++) {
     allDivs[i].className = 'class2';
  }
This doesn't work, since allDivs changes with every iteration of the loop. In this case the best solution to go with is to actually copy the collection into an array and work with the array instead. Something like this:
  function toArray(collection) {
    var result = [];
    var len = collection.length;
    for (var i = 0; i < len; i++) { result[i] = collection[i]; }
    return result; 
  }
  
  var allDivs = document.getElementsByClassName('class1');
  var ar = toArray(coll); //copies a collection into an array
  for(var i = 0; i < ar.length; i++) {
     ar[i].className = 'class2';
  }

Now let's look at another neat solution to this problem (unfortunately not supported by all browsers, i.e Internet Explorer 6 and 7). The document.querySelectorAll() method, is provided as a native DOM method. What's cool with it? You can provide a CSS selector as an argument and it will return you a non-live NodeList, an array-like object containing matching nodes, which will not represent the live structure of the document. Let me note here that in W3C's DOM specifications NodeLists are live, but in the case of querySelectorAll(), its explicitly specified that the returned Nodelist is static (not-live)! So, with this method we don't need to be as verbose as we used to be, e.g instead of calling

var elmts = document.getElementsById('menu').getElementsByTagName('a');
we can do something like:
var elmts = document.querySelectorAll('#menu a');
and of course there's no need to cache/copy variables, in order to access them.

Last but not least...use a library if possible! Avoid dealing with this problem and let the library (e.g YUI, jQuery) do its magic!




References: High Performance JavaScript, Nicholas C. Zakas (http://oreilly.com/catalog/9780596802806)

5 comments:

  1. Another solution for changing all elements with a class name of 'class1' to 'class2' would take advantage of the fact that the HTML collection is live:

    var allDivs = document.getElementsByClassName('class1');
    while (allDivs.length > 0) allDivs[0].className = 'class2';

    ReplyDelete
  2. To convert NodeList to Array in one line:

    var divs = document.getElementsByTagName('div');
    var array = Array.prototype.slice.call(divs);

    ReplyDelete
  3. @Nikolay, sure thats a nice one liner BUT...it won't work in IE6 (what a surprise, right?) :)

    ReplyDelete
  4. @Julian well, yeah that would work too, but I don't think working with an HTML collection is actually the best way to go in any case.
    According to tests performed by Zakas' team using arrays instead of collections is 114x faster in IE6, 191x faster in IE7, 79x faster in IE8, ~15x faster in FF3.5, ~8x faster in Chrome3.
    Hmmm..yep...no reason to work with HTML collections! :)

    ReplyDelete
  5. Hi, Great.. Tutorial is just awesome..It is really helpful for a newbie like me.. I am a regular follower of your blog. Really very informative post you shared here. Kindly keep blogging. If anyone wants to become a Front end developer learn from Javascript Training in Chennai . or learn thru JavaScript Online Training in India. Nowadays JavaScript has tons of job opportunities on various vertical industry. JavaScript Training in Chennai

    ReplyDelete