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; } };