DOM编程
DOM是一个独立于语言的,用于操作XML和HTML文档的接口。
在浏览器中的DOM接口是用js实现的。
浏览器中通常会把DOM和js独立实现。两个相互独立的功能只要通过接口彼此连接就会产生消耗。因此每次js通过接口访问DOM时都会产生一些消耗,操作的次数越多,消耗越大。
DOM访问与修改
根据上面所说的,访问DOM是有代价的。最坏的情况就是在循环中访问或修改元素,尤其是对HTML集合循环操作。
function loop(){
for(var count=0;count<15000;count++){
document.getElementById('here').innerHTML += 'a';
}
}
每一次循环,该页面元素都被访问两次:一次读取innerHTML属性值,一次重写它。
另一种效率更高的方法:用局部变量存储修改中的内容,在循环结束后一次性写入:
function loop(){
var content = '';
for(var count=0;count<15000;count++){
content += 'a';
}
document.getElementById('here').innerHTML += content;
}
结论就是要减少访问DOM的次数。
innerHTML对比DOM方法
有实验结果表示,以下两种方法的性能相差不大:
- 合并HTML字符,然后更新DOM的innerHTML属性
- 使用标准的DOM方法,如createElement、createTextNode等
节点克隆
使用DOM方法更新页面内容的另一个途径是克隆已有元素,而不是更新元素——使用element.cloneNode()而不是document.createElement()。
在大多数浏览器中,节点克隆都更有效率,但也不是特别明显。
HTML集合
HTML集合是包含了DOM节点引用的类数组对象。
想getElementsByName()、getElementsByClassName()等方法都会返回一个HTML集合。
最重要的是,HTML集合是实时更新的,也就是说当底层文档对象更新时,对应的HTML集合与也会更新,一直与文档保持着连接。因此每次需要最新的信息时,都会重复执行查询的过程,哪怕只是获取length也是如此。这正是低效之源。
因此考虑将现有HTML集合的值或者属性缓存到一个局部变量中:
var coll = document.getElementsByTagName('div');
var length = coll.length;
for(var i = 0;i<length;i++){
console.log(coll[i].nodeName)
}
可以想象,如果在每次迭代循环中都要重新获取coll的length和获取HTML结合是多么可怕。像这样缓存集合coll和当前集合的长度length然后在循环中使用就可以有效避免重复查询的性能问题。
遍历DOM
DOM API提供了多种方法来读取文档结构中的特定部份,当你需要从多种方案中选择时,最好为特定操作选择最高效的API。
- 获取DOM元素
当你需要从某一个DOM元素开始,操作周围的元素;或者递归查找所有的子节点,可能会有两种操作方法:
- childNodes获取元素集合
- nextSibling获取相邻元素
在老版IE中,使用nextSibling方法更快。
- 元素节点
DOM元素属性如childNodes,firstChild和nextSibling并不区分元素节点和其他类型节点(比如注释节点和文本节点)。在某些情况下,只需访问元素节点,因此在循环中很可能要检查返回节点的类型并过滤掉非元素节点。这些类型检查和过滤其实是不必要的DOM操作。
有一些API可以只返回元素类型的节点,推荐在浏览器支持的情况下使用这些api。
children =>childNodes
childElementCount =>childNodes.length
firstElementChild =>firstChild
lastElementChild =>lastChild
nextElementSibling => nextSibling
previousElementSibling => previousSibling
- 选择器API
比起getElementById()、getElementsByTagName(),应该选择使用querySelectorAll()方法使用CSS选择器作为参数来获取DOM中特定元素。这种方式查找元素要快得多。
querySelectorAll()返回一个NodeList类数组对象,这个方法不会返回HTML集合,因此返回的节点不会对应实时的文档结构,避免了前面提到的性能问题。
重绘与重排
浏览器下载完页面中的所有组件——HTML标记、js、CSS、图片——之后会解析并生成两个内部数据结构:
- DOM树:表示页面结构
- 渲染树:表示DOM节点如何显示
DOM树中每一个需要显示的节点在渲染树中至少存在一个对应的节点。(隐藏的DOM元素在渲染树中没有对应的节点)
一旦DOM树和渲染树构建完成,浏览器就开始绘制页面元素。
当DOM的变化影响的元素的几何属性(宽和高)——这会引起重排。完成重排后,浏览器会重新绘制受影响的部分到屏幕中,这个过程为重绘。
并不是所有的DOM变化都会影响几何属性,例如改变元素背景色,这只会发生一次重绘,因为元素的布局没有改变。
重绘和重排都是代价昂贵的操作,它们会导致Web应用程序的UI反应迟钝,因此应尽量减少此类操作的发生。
重排何时发生
当页面布局和几何属性改变时就会引起重排:
- 添加或删除可见的元素
- 元素位置改变
- 元素尺寸改变(包括外边距、内边距、边框厚度、宽度、高度等)
- 内容改变(比如文本改变,或图片被另一个不同尺寸的图片替代)
- 页面渲染器初始化
- 浏览器窗口尺寸改变
根据改变的范围和程度,渲染树中或大或小的部分也需要重新计算。有些改变会触发整个页面的重排,比如滚动条出现。
渲染树变化的排队和刷新
由于每次重排都会产生计算消耗,因此大多数浏览器通过队列化修改并批量执行来优化重排过程。
然而有一些情况下你可能就强制刷新了队列并要求计划立刻执行:获取布局信息的操作会立即导致队列刷新:
- offsetTop,offsetLeft,offsetWidth,offsetHeight
- scrollTop,scrollLeft,scrollWidth,scrollHeight
- clientTop,clientLeft,clientWidth,clientHeight
- getComputedStyle( )
以上属性和方法需要返回最新的页面布局信息,因此浏览器不得不执行渲染队列中的所有待处理变化并触发重排以返回正确的值。
在修改样式的过程中,最好避免使用上面列出的属性。他们都会刷新渲染队列。
最小化重绘和重排
为了减少重绘重排的发生次数,应该合并多次对DOM和样式的修改,然后一次处理。
- 改变样式
el.style.borderLeft = "2px";
el.style.borderRight = "1px";
el.style.padding = "5px";
上面的代码中有三个样式属性被改变,每一个都会影响元素的几何结构。最糟糕的情况下会导致浏览器触发三次重排。大部分浏览器都做了优化只会触发一次重排,但在旧浏览器中就会导致三次重排。
一种优化方式是:合并所有的改变并做一次处理,即使用cssText属性。
el.style.cssText = 'border-left:1px;border-right:2px;padding:5px'
另一个一次性修改样式的方法是修改css的class名称,而不是修改内联样式。
- 批量修改DOM
当你需要对DOM元素进行一系列操作时,可以通过以下步骤来减少重绘和重排:
- 使元素脱离文档流
- 对其应用多重改变
- 把元素带回文档中
这个过程中只会触发两次重排:第一步和第三步。如果不按这个步骤来的话,第二步所产生的的任何修改都会触发一次重排。
有三种方法可以使DOM脱离文档:
1. 隐藏元素,应用修改,重新显示
2. 使用document fragment,在当前DOM之外构建一个子树,再把它拷贝会文档
3. 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
缓存布局信息
如前所述,浏览器尝试通过队列化修改和批量执行的方式最小化重排次数。
但当你查询布局信息时,比如获取偏移量、滚动位置或computedStyle时,浏览器为了返回最新值会立即刷新队列并应用所有变更。
最好的做法是尽量减少布局信息的获取次数,获取后把它赋值给局部变量,然后再操作局部变量。
var current = myElement.offsetLeft; //局部变量缓存
myElement.style.left = current +'px';
if(current > 500){
……
}
让元素脱离动画流
一般来说,重排只影响渲染树中的一小部分,但是当页面顶部的一个动画推移页面整个余下的部分时,会导致一次代价昂贵的大规模重排。
使用以下步骤可以避免页面中的大部分重排:
- 使用绝对定位页面上的动画元素,使其脱离文档流
- 让元素动起来。当它扩大时只是覆盖掉部分页面,这只会引起页面一个小区域的重绘过程。
- 动画结束时恢复定位,从而只会下移一次文档的其他元素
事件委托
当页面中存在大量元素,而且每一个都要一次或多次绑定事件处理器时,这种情况可能会影响性能。
解决这种问题的技术就是事件委托。事件能逐层冒泡并被父级元素捕获。使用事件委托,只需给外层元素绑定一个处理器,就可以处理其子元素上触发的所有事件。