JS性能优化——DOM编程

浏览器中的DOM  天生就慢

DOM是个与语言无关的API,它在浏览器中的接口却是用JavaScript实现的。客户端脚本编程大多数时候是在个底层文档打交道,DOM就成为现在JavaScript编码中的重要部分。

DOM访问和修改

ECMAScript 每次访问DOM 都会产生性能损耗。

修改元素则更为昂贵,因为它会导致浏览器重新计算页面的几何变换。

最坏的情况是在循环中访问或修改元素,尤其是对HTML元素集合循环操作。

function innerHtmlLoop(){
    for(var count = 0; count <15000; count++){
            document.getElementById('here').innerHTML +="a";
    }
}

这个函数循环修改页面元素的内容,每次循环迭代,该元素都被访问两次:一次读取innerHtml的属性值,另一次重写它。

换一种效率更高的方法,用局部变量存储修改中的内容,再循环结束后一次性写入:

function innerHtmlLoop(){
    var content = '';
    for(var count = 0; count <15000; count++){    
        count += 'a';
    }
    document.getElementById('here').innerHTML += content;
}

这种方式比上边的快了155倍。

访问DOM的次数越多,代码的运行速度越慢。因此,通用的经验法则是:减少访问DOM的次数,把运算尽量留在ECMAScript这一端处理。

innerHTML对比DOM方法:推荐使用innerHTML 而不是原生DOM方法生成HTML,绝大部分浏览器中都是innerHTML运行的更快。但是对于大多数日常的操作而言,并没有太大的区别,所以根据可读性、稳定性、团队习惯、代码风格来综合决定使用哪种方式。

节点克隆:element.cloneNode()(element表示已有节点)替代document.createElement()。   在大多数浏览器中节点克隆更有效率,但是也不是特别明显。

HTML集合:是包含了DOM节点引用的类数组对象,eg:document.getElementsByName();...或者:document.images页面中所有的img元素document.links所有a元素...

      返回值为HTML集合对象,是个类数组的列表。但是并不是真正的数组(因为没有slice和push之类的方法),但是提供了一个类似数组中的length的属性,并且还能以数字索引的方式访问列表中的元素。

      遍历这种类数组的集合,读取元素集合的length属性会引发集合进行更新,这在所有的浏览器中都有明显的性能问题,优化方法:将集合的长度缓存到循环外的局部变量中,然后在循环的条件退出语句中使用该变量:

function loopCacheLengthCollection(){
    var coll = document.getElementsByTagName('div');  //这里的coll是集合 类数组
        len = coll.length;     //将集合的长度缓存到局部变量len中
    for(var count = 0; count < len; count++){    //不要在这里写 count < coll.length,会明显影响性能 , 
                              //如果coll是数组,那么 count < coll.length 对性能影响并不大
/* 代码处理 */ } }

遍历DOM:可以使用document.querySelector('.myclass')的方法来查询整个文档,活通过elref.querySelector('.myclass')在子树中进行查询,这里的elref是一个DOM元素的引用。

重绘与重排

浏览器在下载完页面中的所有组件---HTML标记、JavaScript、css、图片,之后会解析并生成两个内部数据结构:

DOM树:表示页面结构

渲染树:表示DOM节点如何显示

重排何时发生

    添加或删除可见的DOM元素;

    元素位置改变;

    元素尺寸改变(内外边距,边框厚度,宽高等);

    内容改变;文本改变或者图片被另一个不同尺寸的图片替代

    页面渲染器初始化;

    浏览器窗口尺寸改变

渲染树变化的排队与刷新

    offsetTop, offsetLeft, offsetWidth, offsetHeight

    scrollTop, scrollLeft, scrollWidth, scrollHeight

    clientTop, clientLeft, clientWidth, clientHeight

    getComputedStyle()(currentStyle in IE)

  这些方法需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的“待处理变化”并处罚重排以返回正确的值,在修改样式的过程中,最好避免使用上边列出的属性

var computed,
    tmp = '',
    bodystyle = document.body.style;
if(document.body.currentStyle){ computed = document.body.currentStyle; } else{ computed = document.defaultView.getComputedStyle(document.body,''); } //修改同一属性低效的方式 //然后获取样式信息 bodystyle.color = 'red'; tmp = computed.backgroundColor; bodystyle.color = 'white'; tmp = computed.backgroundImage; bodystyle.color = 'green'; tmp = computed.backgroundAttachment; //每次修改够都读取一个computed样式属性。读取的属性backgroundColor、backgroundImage、backgroundAttachment都与改变的颜色无关。然而浏览器却需要刷新渲染队列并重排,因为compited的样式属性被请求了。 //更有效的方法,性能更快。如下: bodystyle.color = 'red'; bodystyle.color = 'white'; bodystyle.color = 'green'; tmp = computed.backgroundColor; tmp = computed.backgroundImage; tmp = computed.backgroundAttachment;

最小化重绘和重排:重绘和重排可能代价非常昂贵,因此减少此类操作的发生。可以合并多次对DOM和样式的修改,然后依次处理掉。

var el = document.getElementById('mydiv');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
//最糟糕情况下回导致浏览器触发三次重排。大部分浏览器为此做了优化,只会触发一次重排,但是如果在上边代码执行时,有其他代码请求布局信息,那么就会导致三次重排
//而且这段代码四次请求DOM,可以被优化:

var el = document.getElementById('mydiv');
el.style.cssText = 'boeder-left: 1px; border-right: 2px; padding: 5px;';//如果不想覆盖原有的样式 可以写  el.style.cssText += ';boeder-left: 1px;';
//修改css的class名称,更易于维护,可能会带来轻微的性能问题,因为改变类时需要检查级联样式。
var el = document.getElementById('mydiv');
el.className = 'active';

批量修改DOM:当你对DOM元素进行一系列操作时,可以通过下边的步骤来减少重绘和重排:

      1、使元素脱离文档流

      2、对其应用多重改变

      3、把元素带回文档中。

      该过程会触发两次重排(①和③)。但是如果你忽略这两个步骤,那么在第二步所产生的任何修改都会触发一次重排

        使DOM脱离文档的三种基本方法:

          ①隐藏元素,应用修改,重新显示

//为了演示脱离文档的操作,考虑下边的链接列表,它必须更新更多的信息
<ul id = "mylist">
    <li><a href = "http://phpied.com">Stoyan</a></li>
    <li><a href = "http://julienlecomte.com">Stoyan</a></li>
</ul>
//假设附加数据已经存储在一个对象中,并要插入列表。这些数据定义如下:

var data = [
{
"name": "Nicholas",
"url": "http://nczonline.net"
},
{
"name": "Ross",
"url": "http://techfoolery.com"
}
];
//下面是一个用来更新指定节点数据的通用函数:
function appendDataToElement(appendToElement, data){
    var a, li;
    for(var i = 0; max = data.length; i++){
        a = document.createElement('a');
        a.href = data[i].url;
        a.appendChild(document.createTextNode(data[i].name));
        li = document.createElement('li');
        li.appendChild(a);
        appendToElement.appendChild(li);
    }    
}
//最明显的方法:
var ul = document.gerElementById('mylist');
appendDataToElement(ul, data);

//但是这种方法,data的每一个新条目被附加到当前DOM树时都会导致重排。
//第一种方法,改变display属性,临时从文档中移除<ul>元素,然后再回复它:
var ul = document.getElementById('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

          ②使用文档片段在当前DOM之外构建一个子树,再把它拷贝回文档(推荐使用)

var fragment = document.createDocumentFragment();
appendDataToElement(fragment,data);
docuemnt.getElementById(mylist').appendChild(fragment);

          ③将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素

var old = document.getElementById('mylist');
var clone = old.cloneNode(true);
appendDataToElement(old,data);
old.parentNode.replaceChild(clone,old);

缓存布局信息

    myElement.style.left = 1 + myElement.offsetLeft + 'px';
  --->  current++;  myElement.style.left = 1 + current + 'px';

让元素脱离动画流

  一般情况,重排只影响渲染树中的一小部分,但也可能影响很大的部分,甚至整个渲染树。浏览器所需要重排的次数越少,应用程序的响应速度就越快。

  因此当页面的一个动画推移页面整个余下的部分时,会导致一次代价昂贵的大规模重排,用户会感到页面一顿一顿的。渲染树中需要重新计算的节点越多,情况就会越糟。

  拒绝重排:1、使用绝对位置定位页面上的动画元素,将其脱离文档流。

       2、让元素动起来。当它扩大时,会临时覆盖部分页面,但这只是页面一个小区域的重绘过程,不会产生重排并重绘页面的大部分内容。

       3、当动画结束时恢复定位,从而只会下移一次文档的其他元素。

IE和:hover

  从IE7开始,IE允许在任何元素(严格模式)上使用 :hover 这个css伪选择器。但是如果大量使用 :hover,那么会降低响应速度。这个问题在IE8中更为明显。

  很大的表格或很长的列表,应避免使用这种效果。

事件委托

 当页面中存在大量元素,而且每一个都要一次或者多次绑定事件处理器时,这种情况可能会影响性能。每绑定一个事件处理器都是有代价的 。需要访问和修改的DOM元素越多,应用程序也就越慢,特别是时间绑定通常发生在onload(或DOMContentReady)时,此时对每一个富交互应用的网页来说都是一个拥堵的时刻。事件绑定占用了处理的时间,而且,浏览器需要跟踪每个事件处理器,这也会占用更多的内存。当这些工作结束时,这些事件处理器中的绝大部分都不再需要(因为并不是100%的按钮或链接会被用户点击),因此有很多工作是没有必要的。

事件委托可以很好的处理这类问题。原理:事件逐层冒泡并能被父级元素捕获。使用事件代理,只需要给外层元素绑定一个处理器,就可以处理在其子元素上触发的所有事件。

  1、访问事件对象,并判断事件源

  2、取消文档树中的冒泡(可选)

  3、阻止默认动作(可选)

小结:

最小化DOM访问次数,尽可能在JavaScript端处理。

如果需要多次访问某个DOM节点,请使用局部变量存储它的引用

小心处理HTML集合,因为它实时连系着底层文档,把集合的长度缓存到一个变量中,并在迭代中使用它。如果需要经常操作集合,建议把它拷贝到一个数组中。

如果可能的话,使用速度更快的API,比如querySelectorAll()和firstElementChild。

要留意重绘和重排;批量修改样式时,“离线”操作DOM树,使用缓存,并减少访问布局信息的次数

动画中使用绝对定位,使用拖放代理

使用事件委托来减少事件处理器的数量

原文地址:https://www.cnblogs.com/liuyanan/p/10980933.html