布局
在进行 DOM 节点和样式规则匹配的同时,渲染引擎会为每一个可视节点创建 LayoutObject 对象。LayoutObject 对象保存了该节点所需要的布局信息,比如位置( 包含块模型 )、大小( 盒子模型 )等等。整个计算流程大致可以分为以下 4 步:
- 判断该节点是否需要重新布局,如果需要,创建 LayoutObject 对象;
- 计算该节点的宽度和垂直方向的外边距,更新 LayoutObject 对象;
- 遍历该节点的每一个子节点,依次计算它们的布局;
- 根据子节点的布局结果,计算该节点的高度,并最终返回 LayoutObject 对象。
*节点的高度计算,会依据节点类型的不同而采用不同的算法,例如块级元素,定义了自身的宽高,那么就可以据此来确定节点的高度( 期间可能还涉及到溢出时的展示效果 );而内联元素,则需要根据子节点的的大小来确定节点的高度。因此,布局计算通常会采用先确定宽度,再根据子节点的计算结果,确定高度的顺序进行。
渲染引擎的实际布局计算过程,远比我列举的 4 步更为复杂。至于如何做到最高效的计算,这是留给引擎开发者的终极任务,作为前端开发者,简单理解:
DOM + CSSOM = LayoutObject
。
例子:
<style>
body {
margin: 0;
padding: 5px;
}
div {
width: 40px;
height: 40px;
margin: 5px;
}
span {
font-size: 16px;
}
</style>
<body>
<div></div>
<span>hello</span>
</body>
例子中 DOM 和 CSSOM 结合之后,创建出以下 LayoutObject:
body + { width:764px; height: 60px; } = LayoutBlock{ BODY } at( 0,0 ) size( 764*60 )
div + { width:40px; height: 40px; } = LayoutBlock{ DIV } at( 5,5 ) size( 50*50 )
span + { width:37px; height: 20px; } = LayoutInline{ SPAN } at( 5,55 ) size( 37*20 )
*例子中没有设定位置的 CSS 样式,因此全部按照正常流去理解定位。节点大小不够准确,仅供学习。
重排
在布局计算的第一步,就是判断是否需要重新布局。那么,有哪些情况会导致重新布局( 重排 )呢?
首先,网页被初次打开,一定会发生布局计算。
其次,JavaScript 代码通过 DOM、CSSOM 修改节点,致使元素的几何空间发生变化。
最常见的情况有如下几种:
- 添加或删除节点;
- 节点的位置发生变化( 浮动、定位 );
- 节点的大小发生变化( 内容、内外边距和边框宽度 )。
最后,scroll 事件、resize 事件、网页动画( 主要指 JavaScript 动画 )等会造成密集的重排,可能会出现网页卡顿现象。
减少重排的常用方法
重排是比较耗时的,而且,一旦布局发生变化,就会发生重排。我们在实际工作中,可以从以下两个方向,减少重排的发生:
批量修改 DOM
1、使用 display:none 隐藏节点,进行多次修改之后再显示。
例子:
<button>OK</button>
var showBox = (function() {
var div = document.createElement("div");
div.innerHTML = "hello";
div.style.display = "none";
document.body.appendChild(div);
return div;
})();
document.querySelector("button").onclick = function() {
showBox.style.display = "block";
}
2、使用文档片段( DocumentFragment )构建子树,构建完毕之后,添加进 DOM 模型。
例子:
<button>OK</button>
var showList = (function() {
var ul = document.createElement("ul"),
docF = document.createDocumentFragment();
["A", "B", "C"].forEach(function(v) {
var li = document.createElement("li");
li.innerHTML = v;
docF.appendChild(li);
});
ul.appendChild(docF);
return ul;
})();
document.querySelector("button").onclick = function() {
document.body.appendChild(showList);
}
3、使用 innerHTML 和 outerHTML 批量替换 HTML 元素。
例子:
<button>OK</button>
<div></div>
var showList = (function() {
return ul_HTML = "<ul><li>A</li><li>B</li><li>C</li></ul>";
})();
document.querySelector("button").onclick = function() {
document.querySelector("div").innerHTML = showList;
}
*innerHTML 和 outerHTML 的实现原理,其实也是将字符串经过词法、语法分析之后,通过 DocumentFragment 构建子树,并最终添加进 DOM 模型。
批量修改样式
1、使用 className 批量修改节点的样式。
例子:
<style>
p {
font-size: 20px;
color: blueviolet;
border: 1px solid #ccc;
}
.p_style {
font-size: 30px;
color: brown;
border: 2px solid #ccc;
}
</style>
<button>OK</button>
<p>hello</p>
<script>
var change_style = function() {
var p = document.querySelector("p");
p.className = "p_style";
};
document.querySelector("button").onclick = function() {
change_style();
}
</script>
2、另外一个比较不常用的,就是使用 cssText 批量修改样式。
例子:
<style>
p {
font-size: 20px;
color: blueviolet;
border: 1px solid #ccc;
}
</style>
<button>OK</button>
<p>hello</p>
<script>
var change_style = function() {
var p = document.querySelector("p");
p.style.cssText = "font-size: 30px;color: brown;";
};
document.querySelector("button").onclick = function() {
change_style();
}
</script>
Render 树
经过渲染引擎的处理,LayoutObject 对象保存了 DOM 节点以及其对应的布局信息,LayoutObject 对象已经知道了该如何绘制自己。然后,该对象会带着这些信息,插入到 Render 树上。
Render 树按照 DOM 树的排版规则进行排版,但两者并非一一对应的关系,只有 document 节点和可视节点才会出现在 Render 树中。
Render 树可以看作是渲染引擎为最后绘图,而构建的内部表示对象。但实际情况可能更为复杂。因为 HTML5 的出现,让绘图不再是简单地应对 HTML 元素,还有 2D、3D 图形,多媒体等等。
如有错误,欢迎指正,本人不胜感激。