-
重绘和重排
1.1 DOM树和渲染树
浏览器下载完页面中的所有组件、HTML标记,javascript,css图片、之后会解析并生成两个内部的数据结构
DOM树
:
表示页面结构
渲染树
:
表示DOM节点如何显示
DOM树中每一个需要显示的节点在渲染树中至少存在一个对应的节点,(隐藏的DOM元素没有对应的节点).渲染树中的节点被称为帧
或盒
.一旦DOM树和渲染树构建起来,浏览器就会显示(绘制"paint") 页面元素.
1.2 什么是重绘和重排
当DOM的变化改变了元素的几何属性,例如宽和高,或者改变了边框高度,给段落增加文本,等等等.都会导致浏览器需要重新计算元素的几何属性,同样其他的元素的几何属性个位置也会因此受到影响,浏览器会使渲染树受到影响的部分失效, 并重新构建渲染树, 这个过程称为 "重排". 完成重排后, 浏览器会重新绘制受影响的部分到屏幕中,该过程称为"重绘".
并不是所有的DOM变化都会影响几何属性,例如改变一个元素的背景色并不会改变他的宽和高, 在这种情况下,只会发生一次重绘,因为页面的布局并没有改变.
重绘和重排都是代价昂贵的操作,它们会导致web应用程序的UI反应迟钝, 所以尽可能得减少这类操作的发生.
1.3 重排何时发生
- 添加或删除可见的元素
- 元素的位置发生了改变
- 元素的尺寸发生了改变, 包括外边距, 内边距, 边框宽度, 高度等属性被改变
- 内容改变, 例如: 文本改变或者图片被另一个不同尺寸的图片替代
- 页面渲染器初始化
- 浏览器可视区域尺寸改变
根据改变的范围和程度, 渲染树或大或小对应的部分都需要重新计算然后渲染到页面上, 有些改变会触发整个页面的重排,
例如当滚动条出现时.
1.4 最小化重绘和重排
重绘和重排对性能的消耗十分昂贵,因此一个好的提高程序响应速度的方法减少减少此类操作的发生.
为了减少发生次数,应该合并多次对DOM和样式的修改,然后一次性处理掉.
通过DOM改变元素样式
:
let el = document.getElementById("div")
el.style.width = "434px";
el.style.padding = "4px";
el.style.borderLeft = "6px";
示例中三个样式属性被改变, 每一次都影响了元素的几何结构.最糟糕的情况下,会导致浏览器触发三次重排.大部分现代浏览器为此做了优化,只会触发一次重排.但是在旧版本浏览器中仍然效率低下.
一个能够得到同样效果且效率更高的方式是: 合并所有的改变然后一次性处理,这样只会改变DOM一次.使用cssText属性可以实现.
let el = document.getElementById("div")
el.style.cssText = 'width:12px; height:46px; padding:4px'
例子上的代码修改了cssText属性并覆盖了已存在的样式信息, 因此如果向保留现有样式,可以把他附加到cssText字符串后面.
el.style.cssText += 'padding-left:5px'
另外一种一次性改变元素元素的方法是修改css的class类名 ,而不是修改内联样式改变css的Clss名称的方法更清晰,更易于维护.
let el = document.getElementById("div")
el.className = 'style';
el.classList.add("active")
1.5 批量修改DOM
当年需要对DOM元素进行一系列操作时,可以通过以下步骤来减少重绘和重排的次数:
- 使元素脱离文档流
- 对其应用进行多重改变
- 将文档带回文档流
该过程里会触发两次重排---> 第一步和第三步. 如果你忽略第这两个步骤, 那么在第二步所产生的任何修改都会触发一次重排
有三种基本方法可以使DOM脱离文档:
- 隐藏元素, 应用修改, 重新显示
- 使用文档碎片 在DOM树之外构建一个子树,再把它拷贝会文档
- 将原始元素拷贝到一个脱离文档的节点, 修改副本,完成之后在替换原始元素
假设我们有一个普通的列表文本,我们要更新其中的文本内容
<ul id="myList">
<li> text1 </li>
<li> text2 </li>
</ul>
假设数据已经定义到一个数组对象中,要插入这些列表. 这些数据定义如下
let data = [ {text:"text3"}, {text:"text4"}]
下面是是一个用来更新指定节点数据通用函数
function updatePageData(appendEle,data){
let li;
for(let i = 0,max = data.length; i < max;i++){
li = document.createElement("li");
li.textContent = data[i].text;
appendEle.append(li)
}
}
我们通过会使用下面这种正常的方法调用这个函数
let myList = document.getElementById("myList");
updatePageData(myList,dataArr)
然而使用上面这种方法,data数据中的每一次数据被附加到当前DOM树,都会导致一次重排.正如上文所讲到的,一种减少
重排的方法是通过改变display属性, 临时从文档中移除ul元素, 然后在恢复他
let myList = document.getElementById("myList");
myList.style.display = 'none'
updatePageData(myList,dataArr);
myList.style.display = 'block';
另一种改变重排的次数:在文档之外创建一个文档碎片,然后把它附加到原始列表中
let fragment = document.createDocumentFragment();
updatePageData(fragment,dataArr);
myList.append(fragment);
第三种解决方案是为需要修改的节点创建一个备份. 然后对副本进行操作, 一旦操作完成,就有新的节点替代旧的节点.
let myList = document.getElementById("myList");
let clone = myList.cloneNode(true);
updatePageData(clone,dataArr);
myList.parentNode.replaceChild(clone, myList)
我们推荐使用第二个方案( 文档碎片) 因为它所产生的DOM遍历和重排次数最少.
网友评论