构建阶段生成渲染树,渲染树中包含起页面元素结构和每个元素的样式信息。 渲染树作为布局阶段的入参,将元素结构和样式信息计算出元素的位置和尺寸。 布局阶段是一个不断重复递归的阶段,在整个渲染过程耗时较多。
布局流程
布局以渲染树为入参,从根节点开始遍历。
步骤
- 获取盒模型
通过渲染树中的styles属性,获取盒模型
- box: width / height
- padding: padding-top / padding-right / padding-bottom / padding-left
- border: border-top / border-right / border-bottom / border-left
- margin: margin-top / margin-right / margin-bottom / margin-left
- 计算位置和尺寸
获取x/y/widht/padding/border/margin/可见性的过程
-
位置:元素位置依赖于元素的定位属性position
- 静态定位static(default):位置跟随文本流,从上到下从左到右依次定位
- 相对定位relative:初始位置跟随文本流,从上到下从左到右依次定位(同静态定位),再根据偏移量(top/right/bottom/left)获取最终位置
- 绝对定位absolute:依赖 祖先元素(包含块,具体了解absolute定位特性)结合偏移量(top/right/bottom/left) 获取位置
- 固定定位fixed:依赖 视口viewport结合偏移量(top/right/bottom/left) 获取位置
- 粘性定位sticky:由绝对定位和固定定位结合而来,位置依赖于父级滚动元素滚动偏移量
-
尺寸:元素尺寸依赖于元素的展示属性display,不同类型元素获取尺寸方式不同
- 固定尺寸:尺寸为自定义宽高
- 自适应尺寸:尺寸跟随父级宽高或跟随子元素占用空间尺寸(block/inline-block/flex/...)
-
反向调整:跟随子元素占用计算宽高的元素
尺寸跟随子元素占用情况来计算的元素,会暂时不计算尺寸数据,待所有子元素尺寸计算完毕后,再回到当前元素计算其尺寸数据。计算完当前元素尺寸后,计算判断父级是否也需要重新计算,直到根元素。
布局伪代码&布局树示例
function layoutElement(renderNode) {
const element = renderNode.element;
const style = window.getComputedStyle(element);
// 计算内容区尺寸
let width = parseFloat(style.width) || 0;
let height = parseFloat(style.height) || 0;
// 计算内边距
const paddingLeft = parseFloat(style.paddingLeft) || 0;
const paddingRight = parseFloat(style.paddingRight) || 0;
const paddingTop = parseFloat(style.paddingTop) || 0;
const paddingBottom = parseFloat(style.paddingBottom) || 0;
// 计算边框宽度
const borderLeft = parseFloat(style.borderLeftWidth) || 0;
const borderRight = parseFloat(style.borderRightWidth) || 0;
const borderTop = parseFloat(style.borderTopWidth) || 0;
const borderBottom = parseFloat(style.borderBottomWidth) || 0;
// 计算外边距
const marginLeft = parseFloat(style.marginLeft) || 0;
const marginRight = parseFloat(style.marginRight) || 0;
const marginTop = parseFloat(style.marginTop) || 0;
const marginBottom = parseFloat(style.marginBottom) || 0;
// 初始化布局
renderNode.layout = {
width: width,
height: height,
x: renderNode.parent ? renderNode.parent.layout.x + marginLeft + paddingLeft + borderLeft : 0,
y: renderNode.parent ? renderNode.parent.layout.y + marginTop + paddingTop + borderTop : 0
};
// 递归布局子节点,并计算父元素尺寸
let totalWidth = 0;
let totalHeight = 0;
renderNode.children.forEach(child => {
layoutElement(child);
// 根据子元素调整父元素尺寸
totalWidth = Math.max(totalWidth, child.layout.width);
totalHeight += child.layout.height;
});
// 更新父元素尺寸
renderNode.layout.width = width || totalWidth + paddingLeft + paddingRight + borderLeft + borderRight;
renderNode.layout.height = height || totalHeight + paddingTop + paddingBottom + borderTop + borderBottom;
// 处理相对定位
if (style.position === 'relative') {
const top = parseFloat(style.top) || 0;
const left = parseFloat(style.left) || 0;
renderNode.layout.x += left;
renderNode.layout.y += top;
}
}
// 示例渲染树节点结构
const renderTree = {
element: document.documentElement,
children: [
{
element: document.body,
children: []
}
],
layout: {}
};
// 从渲染树根节点开始布局计算
layoutElement(renderTree);
console.log(renderTree);
const layoutTree = {
element: document.documentElement,
layout: {
x: 0,
y: 0,
width: 800,
height: 800,
padding: { top: 0, right: 0, bottom: 0, left: 0 },
borderWidth: { top: 0, right: 0, bottom: 0, left: 0 },
margin: { top: 0, right: 0, bottom: 0, left: 0 },
dispaly: 'block',
visible: 'visible',
},
children: [
{
element: document.body,
layout: {
x: 0,
y: 0,
width: 800,
height: 800,
padding: { top: 0, right: 0, bottom: 0, left: 0 },
borderWidth: { top: 0, right: 0, bottom: 0, left: 0 },
margin: { top: 0, right: 0, bottom: 0, left: 0 },
dispaly: 'block',
visible: 'visible',
},
},
...
],
};