2

本文章记录本人在深入学习Javascirpt DOM中看书理解到的一些东西,加深记忆和并且整理记录下来,方便之后的复习。

减少 DOM 重绘和重排的次数

简单了解重绘和重排

在渲染dom树中为每个需要显示的dom树节点存放至少一个节点,隐藏的dom元素在渲染树中没有对应的节点。然后将页面的元素看作一个具有填、边距、边框和位置的盒子,也就是经常提到的盒模型。如果dom树和渲染树构造完毕,浏览器就会显示或者说是绘制页面的上的元素了。

dom改变影响了元素的几何属性(宽和高)时候,如改变边距宽度或者在段落中添加文字将发生一系列后续动作,浏览器就会重新计算元素的几何属性,而且其他元素的几何属性和位置也会因此收到影响。浏览器是渲染树上受到影响的部分失效,然后重构渲染树,这个过程叫做重排。当重排完毕后,浏览器会在绘制进程中重新绘制屏幕上受到影响的部分。

有一些属性修改是不会影响几何属性的,例如:改变一个元素的背景色不会影响它的高度和宽度。这种情况,只需要重绘,因为元素的布局没有发生变化。

什么情况下会触发重绘和重排

  • 添加或删除可见的dom元素。
  • 元素的位置改变。
  • 元素的尺寸改变。
  • 内容改变,如文本改变或图片被另一个尺寸的图片所代替。
  • 最初的页面渲染。
  • 浏览器窗口大小改变。

某一些改变可能导致重排整个页面,例如:当一个滚动条出现时。

如何减少重排和重绘

由于重排与重绘的代价较高,提高程序响应速度是一个好策略是减少此类的操作发生的机会。

例1,应该将多个dom风格改变后合并到一个批次中一次性执行。

var el = document.getElementById('demo');
el.style.broderLeft = '1px';
el.style.broderRight = '2px';
el.style.padding = '5px';

上面的代码改变了3个样式属性,每次改变都会影响元素的几何属性。导致浏览器重排了3次。现在大多数浏览器都优化了这种情况,只进行一次重排,但是在旧版本的浏览器中,效率非常的底下。

实现相同效果但效率更高的方法:将所有改变合并在一起执行,只修改dom一次。使用cssText

el.style.cssText = 'border-left: 1px;border-right: 2px;padding: 5px';

还有另外一种优化方式就是修改css的类名称,而不是修改元素的内联风格代码。这种方法适合用于那些风格不依赖于运行逻辑且不需要计算的情况下。改变后的css类名称更加清晰,更便于维护。

el.className += ' active';

例2,当需要对dom元素进行多次的修改时,可以通过以下步骤减少重绘和重排次数。

  1. 从文档流中摘除该元素。
  2. 对其应用多重改变。
  3. 将元素带回文档中。

这个过程中引发两次重排:第1步引发一次,第3步引发一次。如果忽略了这两个步骤,那么第2步中每次改变都会引发一次重排。

经历3步后可以将dom从文档中摘除:

  • 隐藏元素
  • 使用一个文档片断在一存在dom之外创建一个子树,然后将它复制到文档。
  • 将原始文档复制到一个脱离文档的节点中,修改副本,然后覆盖原始元素。

避免大部分重排

重排有时候只影响渲染数的一小部分,但也能影响一大部分。甚至整个渲染树。浏览器需要重排ud部分越小。应用程序的响应速度越快,因此,当一个页面顶部的动画推移差不多整个页面时,将引发巨大的重排动作,使用户感到动画不流畅。渲染数的大多数节点需要重新计算,这使情况更加糟糕。

以下步骤可以避免大部分页面进行重排:

  • 使用绝对定位坐标定位页面动画元素,使它位于页面布局流之外。
  • 启动元素动画,当它扩大时候,将会临时覆盖部页面。这是一个重绘过程,但是只能影响页面一小部分,避免重排以及重绘一大块也页面。
  • 当动画结束时候,重新定位。

克隆节点与创建节点哪个好

使用dom方法更新页面内容的另一个方法就是克隆已有的dom元素,而不是创建新的元素。在大多数浏览器上,克隆节点更有效率,但是提高不是很多。如果用克隆节点的方法去创建1000行的表格,只创建一次单元格,然后重复的执行复制操作,这样会快一些。

所以说:克隆节点与创建节都差不多,而克隆节点方法更加适合那些重复操作同一个dom元素。

使用 nextSibling 抓取 DOM

我们经常需要从一个dom元素开始,操作周围的元素,或者递归迭代所有的子节点。这时可以使用childNodes集合或者使用nextSibling获得每个元素的兄弟节点。

var testNextSibling = function(){
    var el = documentgetElementById('demo'), ch = el.firstChild, name = '';
    do {
        name = ch.nodeName; 
    } while (ch = ch.nextSibling);

    return name;
};

var testChildNodes = function(){
    var el = documentgetElementById('demo'), ch = el.childNodes, len = ch.length, name = '';
    for (var i = 0; i < len; i++) {
        name = ch[i].nodeName;
    }
    return name;
};

比较上面两个功能相同的函数,它们都采用非递归方式遍历一个元素的子节点。在不同的浏览器下,这两个函数运行的时间都基本相同,但是在IE下,nextSibling表现比childNode好。在IE6下,nextSiblingchildNode块16倍,在IE7下,nextSibling块105倍。所以说,在旧版本的IE下使用nextsibling抓取dom是首选,在其他的情况下,主要看个人和团队的习惯决定。

使用局部变量来访问集合元素

访问任何类型的dom,当同一个dom属性或者方法被访问同一次以上时,最好使用一个局部变量缓存该dom成员。当遍历一个集合的时候,第一个要优化的就是将集合引用存储在局部变量中,并在循环之外缓存length属性。然后,如果在循环体中多次访问同一个集合元素,那么使用局部变量缓存它。

// bad
var collectionGlobal = function(){
    var coll = document.getElementsByTagName('div'), len = coll.length, name = '';
    for (var i = 0; i < coll; i++) {
        name = document.getElementsByTagName('div')[i].nodeName;
        name = document.getElementsByTagName('div')[i].nodeType;
        name = document.getElementsByTagName('div')[i].tagName;
    }
    return name;
};

// good
var collectionGlobal = function(){
    var coll = document.getElementsByTagName('div'), len = coll.length, name = '', el = null;
    for (var i = 0; i < coll; i++) {
        el = coll[i];
        name = el.nodeName;
        name = el.nodeType;
        name = el.tagName;
    }
    return name;
};

使用 querySelectorAll

使用css选择器是一个便捷的确定节点的方法。许多js库提供类似的API,而且最新的浏览器提供了一个名为querySelectorAll的元素浏览器dom函数。显然这种方法比使用jsdom迭代并缩小元素列表的方法要快。

var el = document.querySelectorAll('#demo a');

当需要联合查询的时候,使用querySelectorAll方法更加的便利。例如,在页面中,一些div元素的class名称是demo1,另一些class名称是demo2,可以使用querySelectorAll一次性获得这两类节点。

var el = document.querySelectorAll('.demo1, .demo2');

如果浏览器支持querySelectorAll方法的话,那么最好就使用它。。还有另一个函数querySelector获取节点,它可以返回符合查询条件的第一个节点。

使用 DOM 结构树托管事件

一个简单而优雅的处理dom事件的技术是事件托管(事件代理)。它基于这样一个事实:事件逐层冒泡总能被父级捕获。采用事件托管技术后,只需要在一个包装元素上连接一个句柄,用与处理子元素发生的所有事件。

  • 捕获
  • 达到目标
  • 冒泡

因为IE不支持事件捕获,所以只要实现托管技术使用冒泡就足够了。

事件托管技术就是只要监听事件侦测事件是不是从目标元素中发出的。里面会有一些跨浏览器的代码,如果将它们移入一个可复用的库里面,那么代码就会变的很干净。跨浏览器部分包括:

  • 访问事件对象,并判断事件缘(目标)。
  • 结束文档树上冒泡。
  • 阻止默认动作(可选)。
HTML: 


<ul id="demo">
    <li>我是第1个li</li>
    <li>我是第2个li</li>
    <li>我是第3个li</li>
</ul>



JS:
var oUl = document.getElementById('demo');

oUl.addEventListener('click', function(e){
    // 兼容 ie
    var e = e || window.event,
        target = e.target || e.srcElement;

    alert(target.innerHTML);
});

最后,如果文章有什么错误和疑问的地方,请指出。与sf各位共勉!


_我已经从中二毕业了
7.9k 声望235 粉丝

不搞前端会死星人。