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 概念。包含块也用于建立子节点的坐标空间。渲染器有xPosyPos坐标,这些坐标是相对于其包含块的。那么包含块到底是什么呢?

以下是 CSS 2.1 规范对该概念的介绍。

我从 WebCore 渲染树的角度引入该概念的方式如下

渲染器的包含块是渲染器的一个祖先块,负责确定该渲染器的位置。

换句话说,当布局在渲染树中递归进行时,块的职责是定位所有将其作为包含块的渲染器。

渲染树的根节点称为RenderView,根据 CSS2.1,这个类对应于初始包含块。如果对Document调用renderer()方法,它也是将被返回的渲染器。

RenderView.h

初始包含块的大小始终与视口相同。在桌面浏览器中,这是浏览器窗口中的可见区域。它也始终位于相对于整个文档的 (0,0) 位置。这是一张图片,说明了文档中初始包含块的位置。黑色边框的框表示RenderView,灰色框表示整个文档。

如果文档滚动,初始包含块将移出屏幕。它始终位于文档顶部,并且大小与视口相同。人们在理解初始包含块时常有的困惑在于,他们期望它以某种方式位于文档之外并且是视口的一部分。

以下是 CSS2.1 中关于包含块的详细规范。

规则可以总结如下

  • 根元素(即 <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);
}

我们将在未来的文章中深入探讨特定的布局方法。