当浏览器下载完页面所需元素(html标记,css层叠样式表,javascript,图片)之后,会生成两个东西:Dom树和渲染树。
Dom树
Dom树,主要是用来表示页面的Dom结构。
渲染树
渲染树主要是用来表示页面是如何进行渲染的。
Dom树中,除了隐藏节点,其余的节点需要与渲染树中的至少存在一个对应的节点。渲染树中的每一个节点,被称为帧或者是盒子。盒子具有内边距,外边距,边框,位置等属性。一旦渲染树构建完成之后,浏览器就开始进行绘制页面。
当Dom的变化影响到了元素的几何属性(宽和高等)——比如说修改了边框的宽度,或者是修改了高度,又或者给文章增加了内容导致元素的高度增加等,会引起浏览器进行重新计算元素的几何属性,同样,其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效。并重新构建渲染树,这个过程称为重排。完成重排之后,浏览器会重新绘制受影响的元素,这个过程被称为重绘。
并不是所有的Dom变化会影响元素的几何属性,例如,改变背景色,不会影响元素的几何属性,因此,这个时候是不会发生重排,仅仅会发生重绘,因为,元素的不布局没有发生变化。重排和重绘的代价都是昂贵的操作,他们会导致浏览器的UI线程卡顿,因此尽可能避免这类操作。
下面就是整个的基本流程图:
什么时候回发生重排
正如前面所说的,当页面的布局和几何属性发生改变的时候,就需要进行重排
。以下的情况也同样会发生重排:
- 添加和或者删除可见的DOM元素
- 元素的位置发生变化
- 元素的尺寸发生变化(包括:外边距,内边距,边框厚度,宽度,高度等属性发生改变)
- 内容发生变化(例如:内容增加引起高度变化或者是图片被另外一个不同尺寸的图片所替换)
- 页面渲染器进行初始化的
- 浏览器窗口尺寸发生改变
根据改变的内容,渲染树中相对应的部分也需要进行计算。有些改变会触发整个页面的重排:例如,当滚动条出现的时候。
由于每次的重排都会产生计算消耗,大多数浏览器通过队列化修改并批量来优化 重排过程。但是,有些时候我们会强制进行刷新队列,并要求计划任务立刻执行。这些方法包括以下 方法:
- offsetTop,offsetLeft,offsetWidth,offsetHeight
- scrollTop,scrollLeft,scrollWidth,scrollHeight
- clientTop,clientLeft,clientWidth,clientHeight
- getComputedStyle()
以上的属性和方法需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的“待处理变化”并触发重排,以返回正确的值。因此,修改样式的过程中,最好避免使用以上的属性或者是方法。
如何优化重排效率
前面说到,重排和重绘的代价其实是非常昂贵的,因此,为了提高程序的响应速度,我们在平时的开发过程中应该尽量减少该操作的发生。为了减少重排或者是重绘的发生次数,我们可以有以下几点的操作。
合并对Dom的多次修改
var el = document.getElementById('mydiv');
el.style.width = '300px';
el.style.height = '400px';
el.style.margin = '15px';
在以上打代码中,我们能够看到对元素的几何属性发生了三次的修改,因此,上面的代码中会触发三次的重排和重绘。因此,我们可以将对元素的三次修改合并成一次修改,这样,就智慧触发一次重排重绘。修改后的代码如下:
var el = document.getElementById('mydiv');
el.style.cssText = 'width:30px;height:400px;margin:15px';
批量修改dom
当我们需要对Dom进行一系列操作时,可以通过以下步骤来减少重绘和重排:
- 是元素脱离文档流
- 对其应用多重改变
- 把元素带回文档中
该过程会触发两次重排,第一步和第三步,如果忽略这两个步骤,那么第二步的修改就会触发多次的重排。这里要说的是,怎么才能使元素脱离文档流?主要的又以下几个方法:
- 隐藏元素,应用修改,重新显示
- 使用文档片段,在当前Dom之外构建一个子树,再把拷贝会文档
- 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
- 使用虚拟Dom
接下来,就演示一下有关如何脱离文档流,来进行批量修改Dom的。我们先来构建一个场景:
<ul id="myList">
<li><a href="https://www.baidu.com">baidu</a></li>
<li><a href="https://www.qq.com">qq</a></li>
</ul>
上面是一个列表,假设我们要向上述列表中添加以下的数据:
var data = [
{
},
{
url: 'https://weibo.com/',
name: '新浪微博'
}
]
如果按照我们习惯性的思维,我们会这么去写:
function appendDataToElement (appendToElement, data) {
var a;
var li;
for (var i = 0; i <data.length; i++) {
a = document.createElement('a');
li = document.createElement('li');
a.href = data[i].url;
a.appendChild(document.createTextNode(data[i].name));
li.appendChild(a);
appendToElement.appendChild(li);
}
}
var appendToElement = document.getElementById('myList');
appendDataToElement(appendToElement, data);
这样写虽说是能够实现我们所想要的功能,但这样做的话,会在每次appendChild之后,引起浏览器的重排和重绘,如果数据量特别大的时候,就会发生很多次的重排和重绘,因此我们需要对上面的方法进行修改:
var appendToElement = document.getElementById('myList');
appendToElement.style.display = 'none';
appendDataToElement(appendToElement, data);
appendToElement.style.display = 'block';
这样话就只用渲染两次,这就是上面所说的隐藏元素,应用修改,重新显示;
接下来实现一下后面的两种(除虚拟dom之外的方法)。
// 创建子树的方法
var appendToElement = document.createDocumentFragment();
appendDataToElement(appendToElement, data);
document.getElementById('myList').appendChild(appendToElement);
// 将原始元素拷贝到一个脱离文档的节点中
var old = document.getElementById('myList');
var clone = old.cloneNode();
appendDataToElement(appendToElement, data);
old.parentNode.replaceChild(clone, old);
针对于以上的方法,这边推荐使用构建子树的方法是创建子树的方法,因为他们所产生的的Dom遍历和重排次数最少,唯一潜在的问题就是文档片段未被充分利用。
还有一种就是虚拟Dom,有关虚拟Dom这个,其实可以参考Vue和React等比较现代的前端开发的内容。