揭露DOM操作以及性能优化

来源:互联网 发布:java孤傲苍狼 博客园 编辑:程序博客网 时间:2024/05/09 09:54

我们都知道:JavaScript DOM操作的效率是很低的,而且不是一般的慢,而且这也是引发性能问题的常见问题之一。为什么会慢呢?因为对DOM的修改为影响网页的用户界面,重绘页面是最昂贵的浏览器操作之一。
在讨论页面重绘、重排之前。需要对页面的呈现流程有些了解,页面是怎么把html结合css等显示到浏览器上的,下面的流程图显示了浏览器对页面的呈现的处理流程。可能不同的浏览器略微会有些不同。但基本上都是类似的。
这里写图片描述
1. 浏览器把获取到的HTML代码解析成一个DOM树,HTML中的每个tag都是DOM树中的节点,根节点就是我们常用的document对象。DOM树里包含了所有HTML标签,包括display:none隐藏,还有用JS动态添加的元素等。
2. 浏览器把所有样式(用户定义的CSS和用户代理)解析成样式结构体,在解析的过程中会去掉浏览器不能识别的样式,比如IE会去掉-moz开头的样式,而FF会去掉_开头的样式。
3、DOM Tree 和样式结构体组合后构建render tree,render tree能识别样式,render tree中每个节点都有自己的样式,而且 render tree不包含隐藏的节点 (比如display:none的节点,还有head节点),因为这些节点不会用于呈现,而且不会影响呈现的,所以就不会包含到 render tree中。注意 visibility:hidden隐藏的元素还是会包含到 render tree中的,因为visibility:hidden 会影响布局(layout),会占有空间。
4. 一旦render tree构建完毕后,浏览器就可以根据render tree来绘制页面了。

回流与重绘

  1. 当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。
  2. 当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。
    注意:回流必将引起重绘,而重绘不一定会引起回流。
    回流何时发生:
    当页面布局和几何属性改变时就需要回流。下述情况会发生浏览器回流:
    1、添加或者删除可见的DOM元素;
    2、元素位置改变;
    3、元素尺寸改变——边距、填充、边框、宽度和高度
    4、内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;
    5、页面渲染初始化;
    6、浏览器窗口尺寸改变——resize事件发生时;
    DOM操作带来的页面重绘或重排是不可避免的,但可以遵循一些最佳实践来降低由于重排或重绘带来的影响。如下是一些具体的实践方法:
    一、 合并多次的DOM操作为单次的DOM操作
    最常见频繁进行DOM操作的是频繁修改DOM元素的样式,代码类似如下:
element.style.color = "red";element.style.fontSize = "20px";element.style.background = "blue";

这种编码方式会因为频繁更改DOM元素的样式,触发页面多次的重排或重绘,建议可以把这些样式放到一个class里面,减少对DOM的操作:

.exstyle{    color: red;    font-size: 20px;    background: blue;}//在js中换成这种写法element.className = "exstyle";

类似的操作还有通过innerHTML接口修改DOM元素的内容。不要直接通过此接口来拼接HTML代码,而是以字符串方式拼接好代码后,一次性赋值给DOM元素的innerHTML接口。
二、把DOM元素离线或隐藏后修改
把DOM元素从页面流中脱离或隐藏,这样处理后,只会在DOM元素脱离和添加时,或者是隐藏和显示时才会造成页面的重绘或重排,对脱离了页面布局流的DOM元素操作就不会导致页面的性能问题。这种方式适合那些需要大批量修改DOM元素的情况。
(1)使用文档碎片(documentfragment)作为那些已创建元素元素的临时容器,最后一次将容器的内容直接添加到父节点中:

var fragment = document.createDocumentFragment(); for(var i=0; i<items.length; i++){      var item = document.createElement("li");      item.appendChild( document.createTextNode("Option"+i);      fragment.appendChild(item);  }  list.appendChild(fragment);  

再看下面一段代码:

for( var i=0; i<items.length; i++){      varitem = document.createElement("li");      item.appendChild(document.createTextNode("Option"+i);      list.appendChild(item);  } 

这段代码的效率是很低的,因为他在每次循环中都会修改当前DOM结构。相比之下,第一段代码只会修改一次当前DOM的结构,就在最后一行,而在这之前,我们用文档碎片来保存那些中间结果。因为文档碎片没有任何可见内容,所以这类修改不会触发回流操作。实际上,文档碎片也不能被添加到DOM中,我们需要将它作为参数传给appendChild函数,而实际上添加的不是文档碎片本身,而是它下面的所有子元素。
(2)通过设置DOM元素的display样式为none来隐藏元素
由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发两次重排。

var myElement = document.getElementById('myElement');myElement.style.display = 'none';    // 一些基于myElement的大量DOM操作...myElement.style.display = 'block';

(3)克隆DOM元素到内存中
这种方式是把页面上的DOM元素克隆一份到内存中,然后再在内存中操作克隆的元素,操作完成后使用此克隆元素替换页面中原来的DOM元素。这样一来,影响性能的操作就只是最后替换元素的这一步操作了,在内存中操作克隆元素不会引起页面上的性能损耗。

var old = document.getElementById('myElement');var clone = old.cloneNode(true);    // 一些基于clone的大量DOM操作...old.parentNode.replaceChild(clone, old);

三、设置具有动画效果的DOM元素的position属性为fixed或absolute
把页面中具有动画效果的元素设置为绝对定位,使得元素脱离页面布局流,从而避免了页面频繁的重排,只涉及动画元素自身的重排了。这种做法可以提高动 画效果的展示性能。如果把动画元素设置为绝对定位并不符合设计的要求,则可以在动画开始时将其设置为绝对定位,等动画结束后恢复原始的定位设置。在很多的 网站中,页面的顶部会有大幅的广告展示,一般会动画展开和折叠显示。如果不做性能的优化,这个效果的性能损耗是很明显的。使用这里提到的优化方案,则可以 提高性能。
四、谨慎取得DOM元素的布局信息
1.缓存DOM对象:因为获取DOM的布局信息会有性能的损耗,所以如果存在重复调用,最佳的做法是尽量把这些值缓存在局部变量中。

for (var i=0; i < len; i++) {    myElements[i].style.top = targetElement.offsetTop + i*5 + 'px';}

如上的代码中,会在一个循环中反复取得一个元素的offsetTop值,事实上,在此代码中该元素的offsetTop值并不会变更,所以会存在不必要的性能损耗。优化的方案是在循环外部取得元素的offsetTop值,相比较之前的方案,此方案只是调用了一遍元素的offsetTop值。

var targetTop = targetElement.offsetTop;for (var i=0; i < len; i++) {    myElements[i].style.top = targetTop+ i*5 + 'px';}

2.因为取得DOM元素的布局信息会强制浏览器刷新渲染树,并且可能会导致页面的重绘或重排,所以在有大批量DOM操作时,应避免获取DOM元素 的布局信息,使得浏览器针对大批量DOM操作的优化不被破坏。如果需要这些布局信息,最好是在DOM操作之前就取得。

var newWidth = div1.offsetWidth + 10;div1.style.width = newWidth + 'px';var newHeight = myElement.offsetHeight + 10; // 强制页面重排myElement.style.height = newHeight + 'px'; // 又会重排一次

如果把取得DOM元素的布局信息提前,因为浏览器会优化连续的DOM操作,所以实际上只会有一次的页面重排出现.

var newWidth = div1.offsetWidth + 10;var newHeight = myElement.offsetHeight + 10;div1.style.width = newWidth + 'px';myElement.style.height = newHeight + 'px';

五、 使用事件托管方式绑定事件
在DOM元素上绑定事件会影响页面的性能,一方面,绑定事件本身会占用处理时间,另一方面,浏览器保存事件绑定,所以绑定事件也会占用内存。页面中 元素绑定的事件越多,占用的处理时间和内存就越大,性能也就相对越差,所以在页面中绑定的事件越少越好。一个优雅的手段是使用事件托管方式,即利用事件冒 泡机制,只在父元素上绑定事件处理,用于处理所有子元素的事件,在事件处理函数中根据传入的参数判断事件源元素,针对不同的源元素做不同的处理。这样就不 需要给每个子元素都绑定事件了,管理的事件绑定数量变少了,自然性能也就提高了。这种方式也有很大的灵活性,可以很方便地添加或删除子元素,不需要考虑因 元素移除或改动而需要修改事件绑定。

// 获取父节点,并添加一个click事件document.getElementById('list').addEventListener("click",function(e) { // 检查事件源元素 if(e.target && e.target.nodeName.toUpperCase == "LI") { // 针对子元素的处理 ...    }});

只在父元素上绑定了click事件,当点击子节点时,click事件会冒泡,父节点捕获事件后通过e.target检查事件源元素并做相应地处理。

原创粉丝点击