在《浏览器底层手记》的前两章中,我们已经拥有了内存里的两棵树:DOM(结构)和 CSSOM(样式)。
但是,如果你现在问浏览器:“那个 <div> 在屏幕的哪个位置?它有多宽?”浏览器其实还是一头雾水。因为此时的 DOM 节点只知道自己是一个“矩形”,却不知道自己该摆在哪里。
这一章,我们将进入渲染流水线中最硬核的数学计算阶段:布局(Layout / Reflow) 。
【布局】Layout 引擎的数学逻辑
如果说 DOM 是骨架,CSSOM 是皮肤,那么 Layout(布局) 就是赋予这具身体在三维空间(虽然大多数时候是二维)中的具体位置和大小。
一、 诞生:从 DOM 到布局树(Layout Tree)
在开始计算坐标之前,浏览器需要先过滤掉那些“看不见”的东西。它会遍历 DOM 树,并结合 CSSOM 生成一棵全新的布局树。
- 过滤不可见节点: 像
<head>标签、script脚本,以及设置了display: none的元素,不会出现在布局树中。 - 处理伪元素: 虽然
::before和::after不在 DOM 里,但由于它们有视觉表现,会被布局引擎“无中生有”地塞进布局树里。 - 注意:
visibility: hidden的元素会出现在布局树中,因为它们虽然看不见,但还占着地儿。
二、 计算:盒模型的几何公式
一旦布局树构建完成,主线程就开始了繁重的几何计算。每个节点都要回答两个核心问题:
- 我的尺寸是多少? (Content + Padding + Border)
- 我的坐标在哪里? (相对于父容器的 x, y 偏移量)
1. 自上而下的约束(Constraints)
布局计算通常是一个递归的过程。父元素会将自己的“可用宽度”传递给子元素,子元素根据这个约束,结合自己的 CSS 属性(如 width: 50% 或 flex: 1)计算出自己的尺寸。
2. 自下而上的反馈(Sizes)
当子元素确定了自己的高度后,会将这个信息“向上汇报”。比如一个父元素没有设置高度,它的高度将由所有子元素堆叠后的总高度决定。
[Image: 描述布局计算的递归方向,父级下传宽度约束,子级上传高度信息]
三、 难点:特殊布局的算法
现代浏览器的布局引擎(如 Chrome 的 LayoutNG)需要处理极其复杂的数学模型:
- 浮动布局(Float): 节点不仅要考虑自己的位置,还要避开周围的浮动元素。
- Flexbox & Grid: 这是布局引擎的重头戏。引擎需要根据剩余空间、对齐方式、增长系数等,进行多次循环计算(Passes)才能确定最终位置。
- 文本折行: 浏览器必须根据字体大小、行高以及容器边界,计算出每一行文本在哪里断开。这是一个非常昂贵的计算过程。
四、 性能杀手:重排(Reflow)
作为前端开发者,你一定听过“减少重排”。通过了解底层原理,你就能明白为什么它这么贵:
当你通过 JS 修改了某个元素的宽度,或者改变了字体大小,浏览器必须:
- 标记脏节点: 将该元素及其受影响的周围元素标记为“Dirty”。
- 重新计算布局树: 重新运行上述的递归算法。
- 连锁反应: 修改一个靠近顶部的元素,往往会导致整个页面的下游节点全部重新计算。
底层视角: 浏览器非常聪明,它会维护一个队列,尝试批量执行布局任务。但如果你在 JS 中连续调用
offsetWidth或getClientRects(),为了给你最准确的值,浏览器会被迫强制刷新队列(Forced Synchronous Layout) ,导致严重的掉帧卡顿。
💡 给前端开发者的硬核贴士
- 脱离文档流的妙用: 对于动画频繁的元素,使用
position: absolute或fixed。这样它的变化只会触发局部的布局计算,而不会牵一发而动全身。 - 避开 Layout,直达 Compositor: 如果可能,尽量通过
transform或opacity来做动画。因为这两个属性不涉及几何坐标的变化,可以完全跳过布局和绘制阶段,直接由 GPU 处理。
结语
现在,每个节点都已经在内存里领到了自己的“门牌号”和“占地面积”。但屏幕依然是白的——因为我们只有坐标,还没有像素。