布局

渲染器在创建完成并添加到渲染树时,并不包含位置和大小信息。计算这些值的过程称为布局(Layout)重排(Reflow)

HTML 采用 基于流的布局模型,这意味着大多数情况下只要一次遍历就能计算出几何信息。处于流中靠后位置元素通常不会影响靠前位置元素的几何特征,因此布局可以按从左至右、从上至下的顺序遍历文档。

  • 坐标系是相对于根节点而建立的,使用的是上坐标和左坐标。
  • 根渲染器的位置左边是 (0, 0),其尺寸为视口(也就是浏览器窗口的可视区域)。

布局是一个 递归 的过程。它从根渲染器(对应于 HTML 文档的 <html> 元素)开始,然后递归遍历部分或所有的渲染器层次结构,每一个渲染器都会通过调用其需要进行布局的子代的 layout 方法,为每一个需要计算的渲染器计算几何信息。任何有可能改变元素位置或大小的样式都会触发这个 Layout 事件。

脏位系统

为避免对所有细小更改都进行整体布局,浏览器采用了一种 Dirty 位系统。如果某个渲染器发生了更改,或者将自身及其子代标注为 dirty,则需要进行布局。类似于脏检测。

dirtychildren are dirty 两种标记方法。children are dirty 表示尽管渲染器自身没有变化,但它至少有一个子代需要布局。

布局方式

全局布局和增量布局

  • 全局布局:指触发了整个渲染树范围的布局,渲染器的 全局样式更改 或者 屏幕大小调整 都会触发全局布局
    • 影响所有渲染器的全局样式更改,例如字体大小更改
    • 屏幕大小调整
  • 增量布局:采用增量方式,也就是只对 dirty 渲染器进行布局(这样可能存在需要进行额外布局的弊端)
    • 当来自网络的额外内容添加到 DOM 树之后,新的渲染器附加到了渲染器中

异步布局和同步布局

全局布局往往是同步触发的。 有时,当初始布局完成之后,如果一些属性(如滚动位置)发生变化,布局就会作为回调而触发。

增量布局是异步执行的。

  • Firefox 将增量布局的 reflow 命令加入队列,而调度程序会触发这些命令的批量执行
  • WebKit 也有用于执行增量布局的计时器:对渲染树进行遍历,并对 dirty 渲染器进行布局。 请求样式信息(例如 offsetHeight)的脚本可同步触发增量布局。

优化方式

浏览器的优化策略

如果布局是由 大小调整渲染器的位置(而非大小) 改变而触发的,那么可以从缓存中获取渲染器的大小,而无需重新计算。在某些情况下,只有一个子树进行了修改,因此无需从根节点开始布局。这适用于在本地进行更改而不影响周围元素的情况,例如在文本字段中插入文本(否则每次键盘输入都将触发从根节点开始的布局)。

因为这个优化方案,所以你每改一次样式,它就不会回流(Reflow)或重绘(Repaint)一次。但是有些情况,如果我们的程序需要某些特殊的值,那么浏览器需要返回最新的值,而会有一些样式的改变,从而造成频繁的回流和重绘。比如获取下面这些值,浏览器会马上进行回流:

  • offsetTopoffsetLeftoffsetWidthoffsetHeight
  • scrollTopscrollLeftscrollWidthscrollHeight
  • clientTopclientLeftclientWidthclientHeight
  • window.getComputedStyle()
  • currentStyle

减少重绘重排的优化策略

  • 减少逐条地修改 DOM 节点的样式,尽可能使用 CSS 类进行批量操作
  • 缓存 DOM 节点,供后面使用
  • 把 DOM 离线后修改,如:documentFragment、虚拟 DOM、改为 display:none 再显示
  • 尽量修改层级比较低的 DOM
  • 有动画的 DOM 使用 fixedabsoultposition,脱离文档流

布局处理

布局通常具有以下模式:

  1. 父渲染器确定自己的宽度
  2. 父渲染器依次处理子渲染器,并且:
    1. 放置子渲染器(设置横纵坐标)
    2. 如果有必要,调用子渲染器的布局(如果子渲染器是 dirty 的,或者这是全局布局,或者出于其他某些原因),这会计算子渲染器的高度
  3. 父渲染器根据子渲染器的累加高度以及边距和补白的高度来设置自身高度,此值也可供父渲染器的父渲染器使用
  4. 将其父 dirty 位设置为 false

宽度计算

渲染器宽度是根据容器块的宽度、渲染器样式中的 width 属性以及边距和边框计算得出的。

例如以下 div 的宽度:

<div style="width: 30%"></div>

将由 Webkit 计算如下(BenderBox 类,calcWidth 方法):

  • 容器的宽度取容器的 availableWidth 和 0 中的较大值。availableWidth 在本例中相当于 contentWidth,计算公式如下:

    clientWidth() - paddingLeft() - paddingRight();

    clientWidthclientHeight 表示一个对象的内部(除去边框和滚动条)。

  • 元素的宽度是 width 样式属性。它会根据容器宽度的百分比计算得出一个绝对值。

  • 然后加上水平方向的边框和补白。

换行

如果渲染器在布局过程中需要换行,会立即暂停布局,并告知其父代需要换行。父代会创建额外的渲染器,并对其调用布局。

参考资料