在web应用中,DOM操作一直属于是最常见的性能瓶颈,优化DOM操作就可以大幅度提升应用的速度,现今火热的React中所使用的虚拟DOM这一卖点也是为了尽量减少DOM操作而存在的优化方案,这一部分我们来具体说一说在DOM编程中的优化方案。
DOM与JavaScript
通常浏览器会将DOM与JavaScript独立实现,那么当我们访问DOM元素的时候实际上是一个独立的功能连接到另一个功能,而这个连接自然会产生性能消耗,每一次的访问都会产生消耗的话,尽可能的减少访问次数则成为优化的必然途径,那么具体有哪些方法呢。
减少DOM访问次数,尽量使用JavaScript处理
举一个简单的例子:
<pre>
//我希望在页面上输出 1-100
错误示范:
for(var i = 1;i≤100;i++){
document.getElementById('p1').innerHTML += ' ' + i;
}
</pre>
上面这段代码当然可以在页面上输出1-100的数字,但是注意,这里每次循环都会调用document.getElementById来访问DOM元素,那这里这个循环就访问了100次DOM。
简单修改一下:
<pre>
var strs = '';
for(var i = 1;i≤100;i++){
strs += ' ' + i;
}
document.getElementById('p1').innerHTML = strs;
</pre>
同样的效果,但是我们只访问了一次DOM,字符串的累积操作我们完全在JavaScript中做。
使用局部变量存储DOM引用
上例子:
//我希望点击一下按钮数字+1
function bindActions(){
document.getElementById('button1').onclick=function(){
document.getElementById('p1').innerHTML = parseInt(document.getElementById('p1').innerHTML) + 1;
//jQuery 的$('#p1').html( parseInt($('#p1').html) + 1 ) 和上面是一样的
}
}
bindActions();
这个例子中,每次点击按钮都会让浏览器重新访问ID为P1的元素并让其原本数字+1,要多次访问的DOM元素,我们应该建立局部变量保存该元素的引用,以此减少DOM的访问。
//修改后
//我希望点击一下按钮数字+1
function bindActions(){
//原生js
var elm_button = document.getElementById('button1'),elm_textbox = document.getElementById('p1');
elm_button .onclick=function(){
elm_textbox .innerHTML = parseInt(elm_textbox .innerHTML) + 1;
}
//jQuery
var $button = $('#button1'),$textbox = $('#p1');
$button.on('click',function(){
$textbox.html( parseInt( $textbox.html() ) + 1 );
})
}
//js与jq的这两个命名方式是我的个人习惯
}
bindActions();
这个优化很简单实在,不过我觉得很多人还是嫌麻烦在用jquery的时候继续直接 $(selector)。。。还是要养成建立多次引用的局部变量好啊!!
为HTML集合做缓存
常见的HTML集合有
- document.getElementsByTagName
- document.getElementsByName
- document.getElementsByClassName
- document.links
- document.images
- document.forms
这些都是返回HTML集合的属性,HTML集合是一个类数组对象,拥有与数组类似的length属性,也能使用数组下标来获取元素。
使用HTML集合的时候,请尽量将其缓存,使用循环语句的时候也要将length缓存( 访问HTML集合的length属性比访问数组的length属性要慢很多 ),举个例子:
//bad
var elms_div = document.getElementsByTagName('div');
for(var i=0;i<elms_div.length;i++){
elms_div[i].innerHTML= i ;
}
//good
var elms_div = document.getElementsByTagName('div');
for(var i=0,len=elms_div.length;i<len;i++){
elms_div[i].innerHTML= i ;
}
另外一点要注意,HTML是具有实时性的,它会与文档一直保持着联系,意思即是你每次使用集合的时候,集合的数据都是最新的,用一段代码解释:
var divs = document.getElementsByTagName('div');
console.log(divs.length); // 3
document.body.appendChild(document.createElement('div'));
console.log(divs.length); //4
每次访问集合的时候他都会重新执行查询DOM的操作来返回最新的集合数据,这是需要注意的。
如果要使用的HTML集合的元素很多并且要频繁操作,可以将集合内的元素全部复制到数组中,数组的速度要比HTML集合快得多( 因为集合与文档时刻保持连接嘛 )
标准浏览器的原生DOM API
现代浏览器的原生JS中已经提供了一些速度更快的原生DOM方法,如querySelectorAll();
这个方法用起来很爽,类似使用css选择器:
var elms = document.querySelectorAll('#d1 p');
不仅方便,而且这个方法返回的不是HTML集合,而是一个类数组的对象NodeList,也正因为它不是HTMl集合,那么它自然就不会有集合的性能问题( 实时连接 )
重绘与重排
这一个点我觉得应该是大部分人都不会关注的,不说关注,就连知道什么是重绘和重排的人都挺少的我估计= =。
什么是重绘
要解释这一点,我觉得我应该先画个图
在DOM树中每个需要被显示的节点(display:none的元素不会在渲染树中)在渲染树中都会有对应的节点,CSS模型定义中,渲染树节点被称为" frames " 或 "boxes" ,上图基本就是浏览器在获取到资源后绘制页面的过程,可能会有点不同不过基本流程都差不多。
当DOM树与渲染树构建完成之后就浏览器就会绘制页面。
假如DOM产生了几何变化,那么与之对应的渲染树中的节点部分以及受到影响的部分都会失效,然后进行重新构建渲染树,这个就是重排的过程
重绘很好理解,在重排之后浏览器重新绘制被影响至失效的部分,这个过程就是重绘
重排与重绘都会对程序UI产生影响,尽量减少重排和重绘就是接下来我们要做的事。
什么时候会重排
布局与几何属性变动的时候浏览器就需要重排,对DOM元素的增加删除、改变位置、改变尺寸、改变内容、浏览器窗口尺寸修改都会产生重排,产生重排之后重绘是必然的,但是反过来,重绘的发生并不一定是因为重排。
只要不对页面布局以及几何属性修改就不需要重排,比如修改颜色只会发生重绘而并不需要重排(滚动条滚动会产生重排的哦)。
有兴趣的同学可以去用下Google的SpeedTracer来观测页面渲染过程
上面说了那么多的重绘和重排,其实就是为了说明在使用JS的时候改变DOM会有什么影响,如此看来,那么优化方案的中心点其实与前面说的很类似,减少DOM操作是关键
合并对DOM元素的修改操作可以优化,其次,减少重排还有下面几种:
- 将元素先隐藏(display:none),更新完毕后再显示出来,举个例子:
常见的列表按条件排序、批量增加或删除纵列,先将其隐藏再排序结束后再放出,会比直接操纵列表省去更多的重排
使用createDocumentFragment() 来更新节点。这个方法比较少人用,createDocumentFragment是一个document对象。它像是一个节点,我们可以朝里面添加子节点,然后使用appendChild(fragment)将其加入目标上,被添加进去的会是fragment的子节点,并且添加到目标对象的过程中只会触发一次重排且只访问一次实时DOM。
为需要修改的节点做一个备份,然后操作副本,操作结束后替换旧的即可。简单概括就是先cloneNode,然后修改clone的,最后replaceChildj即可。
注意DOM动画
利用元素制作一些动画非常常见,这里要提到的是,在做动画的时候要使用absolute,脱离了文档流之后就算元素改变也只是小范围的重排重绘,否则处于文档流中变化的话,会产生大面积的多次重排重绘动作。
事件委托
使用事件委托来减少监听处理器的数量是非常有必要的,大量的事件绑定会让浏览器花费大量的资源来跟踪事件处理器。
父元素可以通过冒泡接收到其下所有元素的事件消息,通过这个特性,我们可以将多数的事件处理器绑定在父元素上,通过筛选是否需要触发的元素来触发事件。
举个例子:
//我想点击页面所有a标签弹出hello
//这里直接用jQ演示。。
//bad
$('a').on('click',function(){
alert('hello')
});
//good
$(document.body).delegate('a','click',function(){
alert('hello')
})
上述代码中两者都实现了点击a标签弹出hello的功能,但是代码二只监听了body就达到了这个效果,而代码一则给每个a标签都绑定了事件监听器,孰优孰劣不言而喻。