DOM编程

操作页面的DOM树是在客户端JavaScript编程中最普遍的行为。这也是导致开发者头疼的最主要原因(这也导致了JavaScript名声不好),因为DOM方法在不同的浏览器中实现得有很多差异。这也是为什么使用一个抽象了浏览器差异的JavaScript库能显著提高开发速度的原因。

我们来看一些在访问和修改DOM树时推荐的模式,主要考虑性能方面。

DOM访问

DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能不好是因为浏览器的DOM实现通常是和JavaScript引擎分离的。从浏览器的角度来讲,这样做是很有意义的,因为有可能一个JavaScript应用根本不需要DOM,而除了JavaScript之外的其它语言(如IE的VBScript)也可以用来操作页面中的DOM。

一个原则就是DOM访问的次数应该被减少到最低,这意味者:

  • 避免在循环中访问DOM
  • 将DOM引用赋给本地变量,然后操作本地变量
  • 当可能的时候使用selectors API
  • 遍历HTML collections时缓存length(见第二章)

看下面例子中的第二个循环,尽管它看起来更长一些,但却要快上几十上百倍(取决于具体浏览器):

  1. // 反模式
  2. for (var i = 0; i < 100; i += 1) {
  3. document.getElementById("result").innerHTML += i + ", ";
  4. }
  5. // 更好的方式 - 更新本地变量
  6. var i, content = "";
  7. for (i = 0; i < 100; i += 1) {
  8. content += i + ",";
  9. }
  10. document.getElementById("result").innerHTML += content;

在下一个代码片段中,第二个例子(使用了本地变量style)更好,尽管它需要多写一行代码,还需要多定义一个变量:

  1. // 反模式
  2. var padding = document.getElementById("result").style.padding,
  3. margin = document.getElementById("result").style.margin;
  4. // 更好的方式
  5. var style = document.getElementById("result").style,
  6. padding = style.padding,
  7. margin = style.margin;

使用selectors API是指使用这个方法:

  1. document.querySelector("ul .selected");
  2. document.querySelectorAll("#widget .class");

这两个方法接受一个CSS选择器字符串,返回匹配这个选择器的DOM列表(译注:querySelector只返回第一个匹配的DOM)。selectors API在现代浏览器(以及IE8+)中可用,它总是会比你使用其它DOM方法来做同样的选择要快。主流的JavaScript库的最新版本都已经使用了这个API,所以你应该去检查你的项目,确保使用的是最新版本。

给你经常访问的元素加上一个id属性也是有好处的,因为document.getElementById(myid)是找到一个DOM元素最容易也是最快的方法。

DOM操作

除了访问DOM元素之外,你可能经常需要改变它们、删除其中的一些或者是添加新的元素。更新DOM会导致浏览器重绘(repaint)屏幕,也经常导致重排(reflow,重新计算元素的位置),这些操作代价是很高的。

还是那句话,原则是尽量少地更新DOM,这意味着我们可以将变化集中到一起,然后在“活动的”(live)文档树之外去执行这些变化。

当你需要添加一棵相对较大的子树的时候,你应该在完成这棵树的构建之后再放到文档树中。为了达到这个目的,你可以使用文档碎片(document fragment)来包含你的节点。

不要这样添加节点:

  1. // 反模式
  2. // 在节点创建后就插入文档
  3. var p, t;
  4. p = document.createElement('p');
  5. t = document.createTextNode('first paragraph');
  6. p.appendChild(t);
  7. document.body.appendChild(p);
  8. p = document.createElement('p');
  9. t = document.createTextNode('second paragraph');
  10. p.appendChild(t);
  11. document.body.appendChild(p);

一个更好的版本是创建一个文档碎片,然后“离线地”(译注:即不在文档树中)更新它,当它准备好之后再将它加入文档树中。当你将文档碎片添加到DOM树中时,碎片的内容将会被添加进去,而不是碎片本身。这个特性非常好用。所以当有好几个没有被包裹在同一个父元素的节点时,文档碎片是一个很好的包裹方式。

下面是使用文档碎片的例子:

  1. var p, t, frag;
  2. frag = document.createDocumentFragment();
  3. p = document.createElement('p');
  4. t = document.createTextNode('first paragraph');
  5. p.appendChild(t);
  6. frag.appendChild(p);
  7. p = document.createElement('p');
  8. t = document.createTextNode('second paragraph');
  9. p.appendChild(t);
  10. frag.appendChild(p);
  11. document.body.appendChild(frag);

这个例子和前面例子中每段更新一次相比,文档树只被更新了一次,只导致一次重排/重绘。

当你添加新的节点到文档中时,文档碎片很有用。当你需要更新已有的节点时,你也可以将这些变化集中。你可以将你要修改的子树的父节点克隆一份,然后对克隆的这份做修改,完成之后再去替换原来的元素。

  1. var oldnode = document.getElementById('result'),
  2. clone = oldnode.cloneNode(true);
  3. // 修改克隆后的节点……
  4. // 结束修改之后:
  5. oldnode.parentNode.replaceChild(clone, oldnode);