DOM(文档对象模型)是一个独立的语言,用于操作XML和HTML文档的程序接口(API)。在游览器中,主要用来与HTML文档打交道,同样也用在Web程序中获取XML文档,并使用DOM API用来访问文档中的数据。尽管DOM是个与语言无关的API,它在游览器中的接口却是用Javascript实现的。客户端脚本编程大多数的是在和底层文档打交道。
DOM的访问和修改是有代价的。打个比方:DOM和js各自为一个岛屿,之间仅有一个收费桥梁连接。每次js访问DOM的时候就相当于途径一次这座桥,并交纳过桥费。访问的次数多了,费用也就高了。这是DOM的访问,而修改的代价就更高了,因为修改DOM会导致游览器重新计算页面的几何变化。最坏的情况就是:在循环中访问或修改元素,尤其是对HTML元素的集合循环操作。
//较慢
function innerHTMLLoop(){
for(var count = 0;count < 15000;count++){
document.getElmentById("id").innerHTML += str;
}
}
//这种方式问题在于每次循环迭代时,该元素都被访问两次:一次读取innerHTML属性值,另一次重写它。
//较快
function innerHTMLLoop2(){
var content = ' ';
for(var count = 0;count < 15000;count++){
countent += str;
}
document.getElementById("id").innerHTML += content;
}
通用法则:减少访问DOM的次数,把运算尽量留在ECMAScript这一端处理。
问题:修改页面区域是用innerHTML属性还是document.createElement()的原生DOM方法好?
答案:相差无几,除开最新版的WebKit内核之外的所有游览器中,innerHTML会更快一些。如果在一个对性能有着苛刻要求的操作中更新一大段HTML,推荐使用innerHTML,因为它在绝大部分游览器中都运行的更快。但大多数日常生活操作而言,并没有太大区别,故应该根据可读性、稳定性、团队习惯、代码风格来综合决定使用哪种方式。
节点克隆:使用DOM方法更新页面内容的另一个途径是克隆已有的元素,而不是创建新元素--换句话说也就是用element.cloneNode()代替document.createElement()。在大多数游览器,节点克隆都更有效率,但也非明显。
HTML集合:HTML集合是包含了DOM节点引用的类数组对象。
以下方法的返回值就是集合:document.getElementsByName() 、document.getElementsByClassName()、document.getElementsByTagName();
以下属性同样返回HTML集合:document.images、document.links、document.forms、document.forms[0].elements;
以上的方法和属性返回值都是HTML集合对象,这是个类数组的列表(并非真正的数组,因为没有push()和slice()之类的方法),但提供了一个类似数组的length属性,并且还能以数字索引的方式访问列表中的元素。DOM标准中定义:HTML集以一种“假死实时态”实时存在,意味着当底层文档对象更新时,它也自动更新。事实上,HTML集合一直和文档保持连接,每次当你需要最新消息时,都会重复执行查询的过程,哪怕只是获取集合的元素个数也是,因此导致性能下降。
//一个意外的死循环
var allDivs = document.getElementsByTagName("div");
for(var i = 0;i < allDivs.length;i++){
document.body.appendChild(document.createElement("div"))
}
这就是因为html集会自动更新导致的一个死循环allDivs.length反应的是底层文档的当前状态,会随着迭代增加。
//读取一个集合的length比读取一个普通数组的length要慢得多,因为每次都要查询。 function toArray(coll){
for ( var i = 0,a = [],len = coll.length ;i<len;i++){
a[i] = coll[i];
}
return a;
}
var coll = document.getElementsByTagName("div");
var arr = toArray(coll); //比较下面两个函数:
//较慢
function loopCollection(){
for(var count = 0;count < coll.length;count++){
//代码处理
}
}
//读取元素集合的length属性会引发集合进行更新,从而提高性能消耗。优化:把集合长度缓存到一个局部变量中,然后在循环的条件退出语句中使用该变量。性能跟loopCoiedArray()一样。
//较快
function loopCopiedArray(){
for(var count = 0;count < arr.length;count++){
//代码处理
}
}
很多情况下如果只需要遍历一个相对较小的集合,缓存length就够了。因为虽然遍历数组比遍历集合快,但是也同时会带来额外的消耗,故因考虑是否值得使用数组拷贝。
访问集合元素时使用局部变量:最慢的版本每次都要读取全局document,优化后的版本缓存了一个集合的引用,最快的版本把当前的集合元素存储到一个变量。
//较慢
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("div")[count].nodeType;
name = document.getElementsByTagName("div")[count].tagName;
}
return name;
}
//较快
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;
}
//最快
function collectionNodesLocal(){
var coll = document.getElementsByTagName("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;
}
遍历DOM
获取DOM元素:childNodes得到元素集合,nextSibling来获取每个相邻元素。
以非递归方式遍历元素子节点:
function testNextSibling(){
var el = document.getElementById("mydiv"),
ch = el.firstChild,name = ' ';
do {
name = ch.nodeName;
}while ( ch = ch.nextSibbling);
return name;
}; function testChildNodes(){
var el = document.getElementById("mydiv");
ch = el.childNodes,
len = ch.length,name = ' ';
for ( var count = 0;count < len;count++){
name = ch[count].nodeName;
}
return name;
};
nextSibling和childNode两种方法运行时间几乎相等,只有在IE里,nextSibling性能更高。
元素节点:在某些情况下,只需访问元素节点,因此在循环中很可能需要检查返回节点的类型并过滤掉非元素节点。这些类型检查和过滤其实是不必要的DOM操作。大部分游览器提供API只返回元素节点。
使用children替代childNodes会更快,因为集合项更少。
选择器API:querySelectorAll()(使用CSS选择器定位节点)原生DOM方法比使用JS和DOM来遍历查找元素要快得多。
重绘和重排:
游览器下载完页面中的所有组件:HTML标记、js、css、图片之后会解析并生成两个内部数据结构:DOM树(表示页面结构)和渲染树(表示DOM节点如何显示)。一旦完成,游览器就开始绘制页面元素。而当DOM的变化影响了元素的几何属性,游览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。这时就发生了重排和重绘。
重排:游览器会使渲染树中受到影响的部分失效,并重新构造渲染树。
发生时间:添加或删除可见DOM元素、元素位置改变、元素尺寸改变、内容改变、页面渲染器初始化、游览器窗口尺寸改变。
重绘:完成重排后,游览器会重新绘制受影响的部分到屏幕中。
最小化重排和重绘:为了减少发生次数,应该合并多次对DOM和样式的修改,然后一次处理。
批量修改DOM:当需要对DOM元素进行一系列操作时,可以通过以下方式减少重排和重绘的次数:使元素脱离文档流、对其应用多重改变、把元素带回文档。
//更新指定节点数据的通用函数
function appendDataToElement(appendToElement,data){
var a,li;
for (var i = 0;max = data.length;i < max;i++){
a = document.createElement("a");
a.href = data.data[i].url;
a.appendChild(document.createTextNode(data[i].name));
li = document.createElement("li");
li.appendChild(a);
appendToElement.appendChild(li);
}
};
//不考虑重排问题
var ul = document.getElementById("myul");
appendDataToElement(ul,data);
//优化,使DOM脱离文档,减少重排
//方法一 隐藏元素,应用修改,重新显示
var ul = document.getElementById("myul");
ul.style.display = "none";
appendDataToElement(ul,data);
ul.style.display = "block";
//方法二 使用文档片断(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档
var fragment = document.createDocumentFragment();
appendDataToElement(fragment,data);
document.getElementById("myul").appendChild(fragment);
//方法三 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
var old = document.getElementById("myul");
var clone = old.cloneNode(true);
appendDataToElement(clone,data);+
old.parentNode.replaceCHhild(clone,old); 注:推荐使用文档片断,因为它们所产生的DOM遍历和重排次数最少。
还有两种情况下优化重排和重绘的方式:在获取布局信息时,缓存布局信息和处理页面动画时,让元素脱离动画流(注:若是大量使用css中:hover这个伪选择器会明显降低响应速度)。
事件委托:
当页面存在大量元素,而且每个都要一次或多次绑定事件处理器时,每绑定一个事件处理器都是有代价的,要么加重了页面负担(更多的代码),要么增加了运行期的执行时间。而一个简单而优雅的处理DOM事件的技术是事件委托。基于:事件逐层冒泡并能被父级元素捕获。
//例如
document.getElementById("menu").onclick = function(e){ //游览器 target
e = e || window.event;
var target = e.target || e.srcElement;
var pageid,hrefparts;
//只关心hrefs,非链接点击则退出
if ( target.nodeName !== "A" ){
return;
} //从链接中找出页面ID
hrefparts = target.href.split("/");
pageid = hrefparts[ hrefparts.length - 1 ];
pageid = pageid.replace(".html"," "); //更新页面
ajaxRequest("xhr.php?page" + id,updatePageContents); //游览器组织默认行为并取消冒泡
if (typeof e.preventDefault === "function"){
e.preventDefault();
e.stopPropagetion();
}else{
e.returnValue = false;
e.cancelBubble = true;
}
};