1.什么是重排和重绘?
首先我们了解一下前因后果:
浏览器下载完页面所有的资源后,就开始构建DOM树,与此同时还会构建渲染树(Render Tree)
注意:在构建渲染树之前,和DOM树一起被构建的还有Style Tree。DOM树和Style Tree 合并成为渲染树
- DOM树:表示页面结构
- 渲染树:表示页面的节点如何显示
一旦渲染树构建完成,就要开始(paint)页面元素。
这里引伸出了重排的概念:
- 当DOM的变化引起了元素几何属性的变化 比如改变元素的宽高,元素的位置。导致浏览器必须重新计算元素的几何属性,并且重新构建渲染树,这个重新计算元素属性并且重新构建渲染树的过程叫做DOM回流,也被称之为重排。完成重排后,要将重新构建的渲染树渲染到屏幕上,这个过程叫做重绘
- 通俗的讲:重排负责元素的几何属性的更新(宽高,位置等)。重绘负责元素的样式更新。
- 重排必定会带来重绘,但是重绘不一定带来重排。当只是改变某个元素的背景时候,就只发生了重绘,没有设涉及重排。
2.重排触发机制
从重排的概念中我们知道,重排发生的根本原理就是元素的几何属性发生变化
- 添加或者删除可见的DOM元素(注意是可见的)
- 元素位置变化
- 元素的尺寸发生变化(例如宽高)
- 元素的内容改变
- 页面渲染器初始化
- 浏览器窗口大小发生改变
3.性能优化
想必大家都知道,为什么要了解重排和重排,因为其严重影响我们网站的性能,代价是非常大的。如果我们不停的改变页面的布局,就会造成浏览器消耗大量的开销在计算页面上,造成页面加载卡顿的情况。用户体验也是极差的。
怎么减少重排的发生呢?
- 关于改变样式
var div = document.querySelector('.div');
div.style.width = '100px';
div.style.background = 'red';
div.style.height = '200px';
我们可以看出,上述代码会触发浏览器两次重排,然而最高效的方法就是合并所有的改变一次处理。这样只修改DOM节点一次。如:cssText属性
var div = document.querySelector('.div');
div.style.cssText = 'width:100px;background: red; height:200px';
爱思考的朋友们可能就会发现,直接改个类名不是更方便吗?没错,还有一种减少重排的方法就是切换类名,而不是使用内联样式的cssText方法。
切换类名就变成了这样:
// css
.active {
width:100px;
height:200px;
background:red;
}
//js
var div = document.querySelector('.div');
div.className = 'active';
这种优化方法只适用于单个存在的节点。
2. 批量修改DOM
实际工作中,我们看到的页面大多数都不是写死的,这样方便后期的维护。所以我们肯定会涉及到DOM的修改。假如我们现在需要动态创建10个节点,这个时候批量修改DOM的优化方式显得尤为重要。
批量修改DOM元素的核心思想:
- 让该元素脱离文档流
- 修改元素
- 统一带回文档中
这个过程引发两次重排,第一步和第三步。如果我们每创建一个节点,就添加到页面中,进行一次计算。这样会把浏览器给累死(开个玩笑)。
注意:我们这里说的脱离文档流不是指css中使用浮动和决定定位,和这个没有关系
话不多说直接上代码:
// html
<ul class='imgList'>
<li><img src='1.jpg'></li>
<li><img src='2.jpg'></li>
</ul>
// js 添加如下信息
let data = [
{ src : '3.jpg'},
{ src : '4.jpg'},
{ src : '5.jpg'},
{ src : '6.jpg'},
{ src : '7.jpg'},
{ src : '8.jpg'},
]
写一个通用方法加载指定的数据:
function appendNode(node ,data) {
var img , li;
for(let i = 0 ; i< data.length; i++){
var li = document.createElement('li');
var img = document.createElement('img');
img.src = data[i].src;
li.appendChild(a);
node.appendChild(li);
}
}
在没有考虑性能的前提下,我们可能会这么写:
let ul = document.querySelector('.imgList');
appendNode(ul,data);
很显然,每创建一个li 和 img 就会添加到页面中。这样会造成多次回流。如果需要加载成百上千个li,必然会造成页面的卡顿。所以我们需要优化。
1)隐藏元素,进行修改后,然后再显示该元素
let ul = document.querySelector('.imgList');
ul.style.display = 'none';
appendNode(ul,data);
ul.style.display = 'block';
将操作的节点隐藏,发生一次重排,最后让节点显示发生一次重排。这种方法只发生了两次重排。相比于上面的方法,性能得到提升。
当元素样式为 **display:‘none’**的时候,元素脱离文档流。重新计算元素属性和重新构建渲染树,发生重排。
2) 使用文档碎片,将我们需要的创建的节点,再文档碎片中创建,然后统一的添加到页面节点中
let frg = document.createDocumentFragment();
appendNode(frg,data);
ul.appendChild(frg);
这个方法很好理解,打个比方:你需要扔垃圾,但是你总不能有一个点垃圾就下楼去大的垃圾桶。所以你买了一个自己的垃圾桶。当垃圾存满是你就一次性的下楼去丢掉。注意:每次丢完垃圾,但是垃圾桶还是会带上楼
为什么我要强调这一点呢?文档碎片是一个轻量级的document对象。它是用来更新,移动节点的。文档碎片有一个好处就是,当向一个节点添加文档片段时候,添加的是文档片段的子节点群,自身不会被添加进去。
3) 将原始元素拷贝到一个独立的节点中,操作这个节点,然后覆盖原始元素
let old = document.querySelector('.imgList');
let clone = old.cloneNode(true);
appendNode(clone,data);
old.parentNode.replaceChild(clone,old);
这种做法只发生了一次重排。
重排非常的消耗性能,我们能做的就是尽量的减小重排的次数。
总结:
1.样式:
- 如果使用行内样式:el.style.cssText = ''
- 直接切换类名
2.批量修改DOM操作: - 节点隐藏
- 文档碎片
- 拷贝覆盖