高性能javascript之DOM编程

DOM编程

对DOM操作代价昂贵,在富应用中通常是一个性能瓶颈。
文档对象模型(DOM)是一个独立于语言,使用XML和HTML文档操作的应用程序接口。在浏览器中,主要与HTML打交道,在网页应用中检索XML文档也很常见。DOM APIs主要用于访问这些文档中数据。
浏览器通常要求DOM实现和Javascript实现保持相互独立,例如在IE中,被称为JScript的Javascript实现位于库文件jscript.dll中,而DOM实现位于另一个库mshtml中。这样导致的问题是:简单来说,两个独立的部分以功能接口连接就会带来性能损耗。一个很形象的比喻是把DOM看成一个岛屿,把javascript看成另一个岛屿,两者之间以一座收费桥连接。每次ECMAScript需要访问DOM时,你需要过桥,交一次过桥费。你操作DOM次数越多,费用就越高。
简单来说,正如前面所讨论的那样,访问一个DOM元素的代价就是交一次过桥费。修改元素的费用可能更贵,因为它经常导致浏览器重新计算页面的几何变化。
为了给你一个关于DOM操作问题的量化印象,考虑下面的例子:

function innerHTMLoop(){
    for(var count = 0;count <= 15000; count++){
        document.getElementById('here').innerHTML += 'a';
    }
} 

此函数在循环中更新页面内容。这段代码的问题是,在每次循环单元中都对DOM元素访问两次:一次是读取innerHTML属性内容,另一次写入它。
一个更有效率版本是将使用局部变量存储更新后的内容,在循环结束后一次性写入:

function innerHTMLLoop2(){
   var content = '';
   for(var count = 0;count <= 15000; count++){
        content += 'a';
    }
    document.getElementById('here').innerHTML += content;

}

HTML集合

//无效的死循环
var alldivs = document.getElementsByTagName('div'); 
for (var i = 0; i < alldivs.length; i++) {
  document.body.appendChild(document.createElement('div'));
}  

//slow
function collectionGlobal() {
var coll = document.getElementsByTagName('div'), 
len = coll.length,
name = '';
for (var count = 0; count < len; count++) {
 name = document.getElementsByTagName('div')[count].nodeName; 
 name = document.getElementsByTagName_r('div')[count].nodeType; 
 name = document.getElementsByTagName_r('div')[count].tagName;
}
 return name; 
};

// faster
function collectionLocal() {
var coll = document.getElementsByTagName('div'), len = coll.length,
name = '';
for (var count = 0; count < len; count++) {
name = coll[count].nodeName; name = coll[count].nodeType; name = coll[count].tagName;
}
return name;
};

//fastest
function collectionNodesLocal() {
var coll = document.getElementsByTagName_r('div'), len = coll.length,
name = '',
el = null;
for (var count = 0; count < len; count++) {
el = coll[count]; name = el.nodeName; name = el.nodeType; name = el.tagName;
}
 return name;
};  

重绘和重排版
当浏览器下载完所有HTML标记,javascript、css,图片之后,它解析文件并创建两个内部数据结构:一个DOM树表示页面结构和一颗渲染树表示DOM节点如何显示。渲染树中为每个需要显示的DOM树节点存放至少一个节点。渲染树的节点称为”框”或者”盒”,符合CSS模型定义,将页面元素看作一个具有填充、边距、边框和位置的盒子。一旦DOM树和渲染数构造完毕,浏览器就可以显示(绘制)页面上的元素了。
当DOM改变影响到元素的几何属性(宽和高)-例如改变了边框高度或者在段落中添加文字,将发生一系列后续动作。浏览器需要重新计算元素的几何属性,而且其他元素的几何属性和位置也会因此改变受到影响。浏览器是渲染数上受到影响的部分失效,然后重构渲染数。这个过程称为重排版,重排版完成时,浏览器在一个重绘进程中重新绘制屏幕上受到影响的部分。
不是所有的DOM改变都会影响几何尺寸。例如,改变一个元素的背景颜色不会影响它的宽度和高度。在这种情况下,只需重绘不需要重排版,因为元素的布局没有改变。
重绘和重排版是负担很重的操作,可能导致网页应用的用户界面失去响应。

以下操作会发生重排版:
添加、删除可见的DOM元素、元素位置改变、元素尺寸改变(边距、填充、边框高度等属性改变)、内容改变、浏览器窗口尺寸改变
因为计算量与每次重排版有关,大多数浏览器通过队列化修改和批量显示优化重排版过程。然后获取布局信息的操作将导致刷新队列的动作。offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、clientTop。改变风格时最好不用使用这些属性。任何一个访问都将刷新渲染队列。

最小化重绘和重排版

//改变风格
var el = document.getElementById('mydiv'); 
el.style.borderLeft = '1px'; 
el.style.borderRight = '2px';
el.style.padding = '5px';

这里改变了三个风格属性,每次改变都影响到元素的几何属性。在这个例子中,它导致浏览器chong排版了三次。一个达到同样效果而效率更高的方法是:将所有改变合并在一起执行,只修改DOM一次。可以通过使用cssText属性实现:

 var el = document.getElementById('mydiv');
 el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';

另一个一次性改变风格的方法是修改css的类名称,而不是改变内联风格代码。

批量修改DOM
有三种基本方法可以将DOM元素从文档中摘除:
1、隐藏元素,进行修改,然后再显示它
2、使用一个文档片段在已存在DOM元素之外创建一个子数,然后将它拷贝到文档中。
3、将原始元素拷贝到一个脱离文档的节点中,修改副本,然后覆盖原始元素。

Fork me on GitHub