浏览器渲染原理-布局阶段

308 阅读3分钟

构建阶段生成渲染树,渲染树中包含起页面元素结构和每个元素的样式信息。 渲染树作为布局阶段的入参,将元素结构和样式信息计算出元素的位置和尺寸。 布局阶段是一个不断重复递归的阶段,在整个渲染过程耗时较多。

布局流程

image.png

布局以渲染树为入参,从根节点开始遍历。

步骤

  1. 获取盒模型

通过渲染树中的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
  1. 计算位置和尺寸

获取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/...)
  • 反向调整:跟随子元素占用计算宽高的元素

image.png

尺寸跟随子元素占用情况来计算的元素,会暂时不计算尺寸数据,待所有子元素尺寸计算完毕后,再回到当前元素计算其尺寸数据。计算完当前元素尺寸后,计算判断父级是否也需要重新计算,直到根元素。

布局伪代码&布局树示例

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',
            },
        },
        ...
    ],
};

浏览器渲染原理-绘制阶段