Layout
在构建了DOM并计算了所有样式之后,下一步是确定所有元素的视觉几何形状。对于这个块级元素我们会计算这个矩形的坐标,对应到这个由元素在content区域占有的几何区域。
在最简单的情况下,布局按DOM顺序,垂直降序排列一个个块。我们称之为“block flow”。即 块流(如下图)
文本以及行内元素比如会生成行内盒,行内盒通常在一行中从左向右流动,这叫做“行内流”,inline-flow。RTL语言,如阿拉伯语和希伯来语,反转了行内流的方向,是从右到左的。
布局要求从计算样式使用字体。 布局使用一个名为HarfBuzz的文本形状库来计算每个字形的大小和位置,这决定了文本运行的总体宽度。字体成形必须考虑排版,比如如字距和(ligatures 连体)
布局可以为一个元素计算多种类型的边界矩形。 例如,当存在溢出时,布局将计算边框框的矩形和布局溢出矩形。如果节点的溢出是可滚动的,布局还计算滚动边界并为滚动条保留空间。最常见的可滚动DOM节点是document节点本身(dom树的根)。
更复杂的布局需要table元素或者更加复杂的布局,例如将内容拆分为多个列,或浮动对象位于一侧,内容在其周围流动,或部分东亚语言的文本是垂直运行而不是水平运行。 注意DOM结构和ComputedStyle值是如何输入到布局算法的。每个管道阶段使用的是前几个阶段的结果。
布局操作在一个单独的树上(layout tree),这个树与DOM相关联,layout tree的节点实现yin法,LayoutObject有不同的子类(block,text...),这取决于所需的布局行为。样式的更新阶段也会去构建布局树,布局阶段遍历布局树,计算每个LayoutObject的视觉几何形状,并对每个LayoutObject执行布局。
在一般的情况下,一个DOM节点是对应一个LayoutObject,但有时候,一个DOM节点没有对应的LayoutObject,也有的时候一个LayoutObject没有对应的DOM节点,也有的时候,一个DOM节点可以对应多个LayoutObject。(如果一个容器盒子,里面如果有了一个块级盒子,那么,他里面便只能有块级盒子)
- 如上图,一个容器里面有了 div和span,span是行内元素,div是块级元素,于是,为了保证容器里面只有块级盒子,所以会在span的布局对象外面包裹一层匿名的块级盒子。这就是一个LayoutObject没有对应的DOM节点的情况
- 以及如果一个盒子的计算属性中的display属性是none,那么它也没有对应的LayoutObject。
这个布局树的构造是基于一个叫做FlatTreeTraversal(平面树遍历的算法)。(ps:可以深入研究)
现在的布局引擎正在重写,现在的布局树中保留着上一代的布局对象和 新一代布局对象(NG = next generation),当然在最后,所有的布局对象都会变成新一代的布局对象,上一代的布局对象中保留着输入输出,以及布局的算法,这可以看到整棵树的状态。在下一代中,布局的输入和输出都是有清晰的分隔的,输出是一个不可更改,可缓存的布局结果。
布局结果会指向一个描述物理几何的片段树。
Example
以下举个例子:
左上角是代码,右下角是效果,
那么这个代码会对应什么样子的DOM呢。
<div style="max-width: 100px"> <div style="float: left; padding: 1ex">F</div> <br>The <b>quick brown</b> fox <div style="margin: -60px 0 0 80px">jumps</div> </div>
没错,就是左侧这样的DOM树,那它会对应怎么样的LayoutTree结构呢。
对应的布局树的结果是这样的,这里需要我解释一下,首先这个容器div里面有块级元素div,所以,为了保证容器盒子里面只有块级元素,所以会用一个匿名的块级盒子将其包裹起来,(这里注意,第一个div因为具有浮动属性,所以它也已经不是块级元素了)
下一步是什么,没错,就是上面说到的,他会计算生成一棵片段树,里面会有描述物理几何的信息。
Box (block-flow) at 0,0 100x12 Box (block-flow children-inline) at 0,0 100x54 LineBox at 24.9,0 0x18 Box (floating block-flow children-inline) at 0,0 24.9x34 LineBox at 8,8 8.9x18 Text 'F' at 8,8 8.9x17 Text '\n' at 24.9,0 0x17 LineBox at 24.9,18 67.1x18 Text 'The ' at 24.9,18 28.9x17 Text 'quick' at 53.8,18 38.25x17 LineBox at 0,36 69.5x18 Text 'brown' at 0,36 44.2x17 Text ' fox' at 44.2,36 25.3x17 Box (block-flow children-inline) at 80,-6 20x18 LineBox at 0,0 39.125x18 Text 'jumps' at 0,0 39.125x17
这棵树大概就是这样的,上面会有坐标以及宽高等信息。
Paint绘制
经过上面那一大堆流程,现在我们已经理解了布局对象的几何形状,是时候把他们绘制出来了。Paint这个流程会将绘制的操作记录在显示项(display items)列表中。绘制操作可能类似于“在这些坐标处以这种颜色绘制矩形”。每个布局对象可能有多个显示项(display items),对应其视觉外观的不同部分,如背景、前景、轮廓等。
重要的是要按照正确的顺序绘制元素,以便在重叠时能够正确地堆叠。顺序可以通过style来控制(z-index),如下图,举个例子(注意:z-index只能在定位元素上生效,真的是边写例子,变解决知识漏洞,嘿嘿)。
上面代码对应的DOM树是这样的,但是在Paint绘制的时候,并不是按照DOM顺序,先画黄的,再画绿的,而是按照栈的顺序,如上面效果,是先画的底层的绿色,再去绘制的黄色的方块。
一个元素甚至可以部分在另一个元素的前面,部分在另一个元素的后面。这是因为绘制在多个阶段中运行,每个绘制阶段都对子树进行自己的遍历。每个绘制阶段都是一个堆叠上下文的单独遍历。
比如上面这个例子,虽然蓝色是再绿的后面,但是因为foregrounds是在background之后,于是,文字就在最前面了。
Paint - example
上面是代码和效果,那么它在绘制阶段的display items是什么样的呢
就是首先画 根节点 document,之后对盒子进行绘制,之后绘制前景色。其中文本运行的绘制操作包含一个blob,其中包含每个符号的标识符和偏移量。
结语
这样就讲完了浏览器渲染机制的前半段,下一篇会从光栅化开始讲,整个浏览器渲染机制会有2-3篇文章,也会有其他的延申。