WebCore 渲染 III – 布局基础
渲染器首次创建并添加到树中时,它们还没有位置或大小。确定所有盒子的位置和大小的过程称为布局。所有渲染器都有一个layout
方法。
void layout()
布局是一个递归操作。一个名为FrameView
的类表示文档的包含视图,它也有一个layout
方法。帧视图负责管理渲染树的布局。
FrameView
可以执行两种类型的布局。第一种(也是迄今为止最常见的一种)是整个渲染树的布局。在这种情况下,调用渲染树根节点的布局方法,然后整个渲染树都会更新。第二种布局类型仅限于渲染树的特定子树。它用于某些小型子树的布局不可能影响其周围环境的情况。目前,子树布局仅用于文本字段(但将来可能用于 overflow:auto 块和其他类似结构)。
脏位
布局使用脏位系统来确定对象是否确实需要布局。每当新的渲染器插入到树中时,它们会标记自身以及其祖先链中的相关链接为脏。渲染树使用了三个独特的位。
bool needsLayout() const { return m_needsLayout || m_normalChildNeedsLayout || m_posChildNeedsLayout; } bool selfNeedsLayout() const { return m_needsLayout; } bool posChildNeedsLayout() const { return m_posChildNeedsLayout; } bool normalChildNeedsLayout() const { return m_normalChildNeedsLayout; }
第一个位用于渲染器自身脏时,可以使用方法selfNeedsLayout
查询。每当此位设置为true
时,相关的祖先渲染器也会设置位,表明它们有一个脏的子节点。设置的位类型取决于链中前一个链接被标记为脏时的定位状态。posChildNeedsLayout
用于指示定位子节点被标记为脏。normalChildNeedsLayout
用于指示正常流子节点被标记为脏。通过区分这两种子节点类型,可以优化仅有定位元素移动的情况下的布局。
包含块
“相关的祖先链”到底是什么意思?当对象被标记为需要布局时,被标记为脏的祖先链基于一个称为包含块的 CSS 概念。包含块也用于建立子节点的坐标空间。渲染器有xPos
和yPos
坐标,这些坐标是相对于其包含块的。那么包含块到底是什么呢?
我从 WebCore 渲染树的角度引入该概念的方式如下
渲染器的包含块是渲染器的一个祖先块,负责确定该渲染器的位置。
换句话说,当布局在渲染树中递归进行时,块的职责是定位所有将其作为包含块的渲染器。
渲染树的根节点称为RenderView,根据 CSS2.1,这个类对应于初始包含块。如果对Document
调用renderer()
方法,它也是将被返回的渲染器。
初始包含块的大小始终与视口相同。在桌面浏览器中,这是浏览器窗口中的可见区域。它也始终位于相对于整个文档的 (0,0) 位置。这是一张图片,说明了文档中初始包含块的位置。黑色边框的框表示RenderView
,灰色框表示整个文档。
如果文档滚动,初始包含块将移出屏幕。它始终位于文档顶部,并且大小与视口相同。人们在理解初始包含块时常有的困惑在于,他们期望它以某种方式位于文档之外并且是视口的一部分。
规则可以总结如下
- 根元素(即 <html> 元素)的渲染器将始终将 RenderView 作为其包含块。
- 如果渲染器的 CSS position 是 relative 或 static,则包含块将是渲染树中最接近的块级祖先。
- 如果渲染器的 CSS position 是 fixed,则包含块将是 RenderView。技术上,RenderView 不充当视口,因此 RenderView 必须调整固定定位对象的坐标以考虑文档滚动位置。与其为视口单独设置一个渲染器,不如在这种情况下让 RenderView 像一个视口包含块那样运作,这样更简单。
- 如果渲染器的 CSS position 是 absolute,则包含块是最近的、position 不是 static 的块级祖先。如果不存在这样的祖先,则包含块将是 RenderView。
渲染树有两个便捷方法,用于查询对象是否具有 absolute、fixed 或 relative 的 position。它们是
bool isPositioned() const; // absolute or fixed positioning bool isRelPositioned() const; // relative positioning
在大多数代码中,术语positioned指的是 CSS 中的 absolute 和 fixed 定位对象。术语relPositioned指的是 CSS 中的 relative 定位对象。
渲染树有一个方法用于获取渲染器的包含块。
RenderBlock* containingBlock() const
当对象被标记为需要布局时,它会沿着容器链向上遍历,设置normalChildNeedsLayout
位或posChildNeedsLayout
位。该对象的isPositioned链中前一个链接的状态决定了设置哪个位。容器链大致对应于包含块链,尽管中间的行内元素也会被标记为脏。由于这种区别,使用了另一个名为container
的方法而不是containingBlock
来确定脏标记链。
RenderObject* container() const
layoutIfNeeded 和 setNeedsLayout(false)
layoutIfNeeded
方法(术语上类似于 AppKit 的 displayIfNeeded 方法)是一种方便的简写方式,用于告诉渲染器仅在其设置了脏位时才执行布局。
void layoutIfNeeded()
所有布局方法通常以setNeedsLayout(false)
结束。在离开布局方法之前清除渲染器上的脏位非常重要,这样未来的布局调用就不会错误地认为对象仍然是脏的。
布局方法的剖析
从高层面看,布局方法通常看起来像这样
void layout() { ASSERT(needsLayout()); // Determine the width and horizontal margins of this object. ... for (RenderObject* child = firstChild(); child; child = child->nextSibling()) { // Determine if the child needs to get a relayout despite the dirty bit not being set. ... // Place the child. ... // Lay out the child child->layoutIfNeeded(); ... } // Now the intrinsic height of the object is known because the children are placed // Determine the final height ... setNeedsLayout(false); }
我们将在未来的文章中深入探讨特定的布局方法。