首页 » 编写高质量代码:改善JavaScript程序的188个建议 » 编写高质量代码:改善JavaScript程序的188个建议全文在线阅读

《编写高质量代码:改善JavaScript程序的188个建议》建议117:减少DOM重绘和重排版次数

关灯直达底部

浏览器在完成所有页面HTML标记、JavaScript、CSS、图片下载后,将解析文件并创建两个内部数据结构。

❑一棵DOM树:表示页面结构。

❑一棵渲染树:表示DOM节点如何显示。

在渲染树中为每个需要显示的DOM树节点存放至少一个节点(隐藏的DOM元素在渲染树中没有对应节点)。将渲染树上的节点称为“框”或者“盒”,符合CSS模型的定义,将页面元素看做一个具有填充、边距、边框和位置的盒。一旦DOM树和渲染树构造完毕,浏览器就可以显示(绘制)页面上的元素了。

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

不是所有的DOM改变都会影响几何属性。例如,改变一个元素的背景颜色不会影响它的宽度或高度。在这种情况下,只需要重绘(不需要重排版),因为元素的布局没有改变。

重绘和重排版是负担很重的操作,可能导致网页应用的用户界面失去响应。因此,应尽可能减少这类事情的发生。当布局和几何发生改变时需要重排版。在下述情况中会发生重排版:

❑添加或删除可见的DOM元素。

❑元素位置改变。

❑元素尺寸改变(因为边距、填充、边框宽度、宽度和高度等属性改变)。

❑内容改变,如文本改变或图片被另一个不同尺寸的图片所替代。

❑最初的页面渲染。

❑浏览器窗口改变尺寸。

根据改变的性质,渲染树上或大或小的一部分需要重新计算。某些改变可能导致重排版整个页面,如当一个滚动条出现时。

因为计算量与每次重排版有关,因此大多数浏览器都通过队列化修改和批量显示来优化重排版过程。然而,可能经常不由自主地强迫队列进行刷新并要求立刻应用所有计划改变的部分。获取布局信息的操作将导致刷新队列动作,这意味着使用了下面这些方法:

❑offsetTop、offsetLeft、offsetWidth、offsetHeight

❑scrollTop、scrollLeft、scrollWidth、scrollHeight

❑clientTop、clientLeft、clientWidth、clientHeight

❑getComputedStyle(在IE中此函数称为currentStyle)

布局信息是由这些方法返回最新的数据,浏览器不得不运行渲染队列中待改变的项目并重新排版以返回正确的值。

在改变样式的过程中,最好不要使用前面列出的那些属性。任何一个访问都将刷新渲染队列,即使正在获取那些最近未发生改变的或与最新的改变无关的布局信息。例如,下面示例改变同一个风格属性3次。


var computed,tmp=/'/',bodystyle=document.body.style;

if(document.body.currentStyle){//IE,Opera

computed=document.body.currentStyle;

}else{//W3C

computed=document.defaultView.getComputedStyle(document.body,/'/');

}

bodystyle.color=/'red/';

tmp=computed.backgroundColor;

bodystyle.color=/'white/';

tmp=computed.backgroundImage;

bodystyle.color=/'green/';

tmp=computed.backgroundAttachment;


在上面代码中,body元素的前景色被改变了3次,在每次改变之后都导入了computed的风格。导入的属性backgroundColor、backgroundImage和backgroundAttachment与颜色改变无关。然而,浏览器需要刷新渲染队列并重排版,因为computed的风格是被查询而引发的。

更好的方法是不要在布局信息改变时查询computed风格。如果将查询computed风格的代码移到末尾,那么在所有浏览器上都会执行得更快。


bodystyle.color=/'red/';

bodystyle.color=/'white/';

bodystyle.color=/'green/';

tmp=computed.backgroundColor;

tmp=computed.backgroundImage;

tmp=computed.backgroundAttachment;


由于重排版和重绘代价较高,因此,提高程序响应速度的一个好策略是减少此类操作发生的机会。为减少发生次数,应该将多个DOM和风格改变后合并到一个批次中一次性执行。


var el=document.getElementById(/'myp/');

el.style.borderLeft=/'1px/';

el.style.borderRight=/'2px/';

el.style.padding=/'5px/';


上面代码中改变了3个样式属性,每次改变都影响到元素的几何属性,导致浏览器重排版了3次。目前大多数浏览器都优化了这种情况,只进行一次重排版,但在旧版本浏览器中,效率将十分低下。如果其他代码在这段代码运行时查询布局信息,将导致3次重布局发生。而且,此代码访问DOM 4次,可以被优化。

实现相同效果但效率更高的方法:将所有改变合并在一起执行,只修改DOM一次。具体可通过使用cssText属性实现:


var el=document.getElementById(/'myp/');

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


在这个示例中,修改cssText属性,覆盖已存在的风格信息。如果打算保持当前的风格,那么可以将它附加在cssText字符串的后面。


el.style.cssText+=/';border-left:1px;/';


另一个方法是修改CSS的类名称,而不是修改内联风格代码。这种方法适用于那些风格不依赖于运行逻辑且不需要计算的情况。改变后的CSS类名称更清晰,更易于维护,虽然它可能带来轻微的性能冲击,但是有助于保持脚本免除显示代码。


var el=document.getElementById(/'myp/');

el.className=/'active/';


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

第1步,从文档流中摘除该元素。

第2步,对其应用多重改变。

第3步,将元素带回文档中。

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

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

❑隐藏元素,进行修改,然后再显示它。

❑使用一个文档片断在已存DOM之外创建一个子树,然后将它复制到文档中。

❑将原始元素复制到一个脱离文档的节点中,修改副本,然后覆盖原始元素。

下面示例中有一个链接列表,它必须被更多的信息所更新。


<ul>

<li><a href=/"#/">链接1</a></li>

<li><a href=/"#/">链接2</a></li>

</ul>


假设附加数据已经存储在一个对象中了,需要将其插入到这个列表中。这些数据定义如下:


var data=[{

/"name/":/"链接3/",

/"url/":/"#/"

},{

/"name/":/"链接4/",

/"url/":/"#/"

}];


下面是一个通用的函数,用于将新数据更新到指定节点中:


function appendDataToElement(appendToElement,data){

var a,li;

for(var i=0,max=data.length;i<max;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.getElementById(/'mylist/');

appendDataToElement(ul,data);


然而,将data队列上的每个新条目追加到DOM树都会导致重排版。第一种减少重排版的方法:改变display属性,临时从文档上移除<ul>元素然后再恢复它。


var ul=document.getElementById(/'mylist/');

ul.style.display=/'none/';

appendDataToElement(ul,data);

ul.style.display=/'block/';


第二种减少重排版的方法:在文档之外创建并更新一个文档片断,然后将它附加在原始列表上。文档片断是一个轻量级的document对象,它被设计用于更新、移动节点之类的任务。文档片断一个便利的语法特性:在向节点附加一个片断时,实际添加的是文档片断的子节点群,而不是文档片断自己。下面的例子减少一行代码,只引发一次重排版。


var fragment=document.createDocumentFragment;

appendDataToElement(fragment,data);

document.getElementById(/'mylist/').appendChild(fragment);


第三种减少重排版的方法:首先创建要更新节点的副本,然后在副本上操作,最后用新节点覆盖老节点。


var old=document.getElementById(/'mylist/');

var clone=old.cloneNode(true);

appendDataToElement(clone,data);

old.parentNode.replaceChild(clone,old);


尽可能使用文档片断(第二种方法)来减少重排版,因为它涉及最少数量的DOM操作和重排版。唯一潜在的隐患:当前文档片断还没有得到充分利用。

浏览器通过队列化修改和批量运行的方法,尽量减少重排版次数。当查询布局信息如偏移量、滚动条位置或风格属性时,浏览器刷新队列并执行所有修改操作,以返回最新的数值。应尽量减少对布局信息的查询,查询时将查询次数赋给局部变量,并通过局部变量参与计算。

例如,将元素myElement向右下方向平移,每次一个像素,起始于100像素×100像素位置,结束于500像素×500像素位置,在timeout循环体中可以使用。


myElement.style.left=1+myElement.offsetLeft+/'px/';

myElement.style.top=1+myElement.offsetTop+/'px/';

if(myElement.offsetLeft>=500){

stopAnimation;

}


这样做很没有效率,因为每次元素移动,代码查询偏移量,就会导致浏览器刷新渲染队列,并不会从优化中获益。还有一个办法,只需要获得起始位置值一次,将它存入局部变量中(var current=myElement.offsetLeft;),然后在动画循环中使用current变量而不再查询偏移量。


current++

myElement.style.left=current+/'px/';

myElement.style.top=current+/'px/';

if(current>=500){

stopAnimation;

}


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

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

❑使用绝对坐标定位页面动画的元素,使它位于页面布局流之外。

❑启动元素动画,当它扩大时,将会临时覆盖部分页面。这是一个重绘过程,但只影响页面的一小部分,避免重排版及重绘一大块页面。

❑当动画结束时,重新定位。