从 Chrome 源码看浏览器如何 layout 布局 - 前端加油站 - 科蚁网
请选择 进入手机版 | 继续访问电脑版

从 Chrome 源码看浏览器如何 layout 布局

假设有以下html/css:

<div style="border:1px solid #000; width:50%; height: 100px; margin: 0 auto"></div>

这在浏览器上面将显示一个框:

为了画出这个框,首先要知道从哪里开始画、画多大,其次是边缘stroke的颜色,就可以把它画出来了:

void draw(SkCanvas* canvas) {
    SkPaint paint;
    paint.setStrokeWidth(1);
    //从位置为(200, 200)的地方开始画,宽度为400,高度为100
    SkRect rect = SkRect::MakeXYWH(200, 200, 400, 100);
    canvas->drawRect(rect, paint);
}

上面是用Skia画的代码,Skia是一个跨平台的开源2D图形库,是Chrome/firefox/android采用的底层Paint引擎。

为了能够获取到具体的值,就得进行layout。什么叫layout?把css转化成维度位置等可直接用来描绘的信息的过程就叫layout,如下Chrome源码对layout的解释:

// The purpose of the layout tree is to do layout (aka reflow) and store its
// results for painting and hit-testing. Layout is the process of sizing and
// positioning Nodes on the page.

从Chrome源码看浏览器如何计算CSS》这篇文章介绍了怎么把css转化成ComputedStyle,上面的div,它被转化后的style如下所示:

width的大小是50,类型是百分比,而margin值是0,类型是auto,这两种都不能直接用来画的。所以需要通过layout计算出具体的数字。

 1. 建立layout树

从Chrome源码看浏览器如何构建DOM树》这篇文章介绍了如何html文本的过程。当解析完收到的html片段后,会触发Layout Tree的构建:

void Document::finishedParsing() {
      updateStyleAndLayoutTree();
}

每个非display:none/content的Node结点都会相应地创建一个LayoutObject,如下blink源码的注释:

// Also some Node don't have an associated LayoutObjects e.g. if display: none
// or display: contents is set.

并建立起它们的父子兄弟关系:

LayoutObject* newLayoutObject = m_node->createLayoutObject(style);
parentLayoutObject->addChild(newLayoutObject, nextLayoutObject);

形成一棵独立的layout树。

当layout树建立好之后,紧接着用style计算layout的值。

2. 计算layout值

以上面的div为例,它需要计算它的宽度和margin。

(1)计算宽度

宽度的计算是根据数值的类型:

switch (length.type()) {
  case Fixed:
    return LayoutUnit(length.value());
  case Percent:
    // Don't remove the extra cast to float. It is needed for rounding on
    // 32-bit Intel machines that use the FPU stack.
    return LayoutUnit(
        static_cast<float>(maximumValue * length.percent() / 100.0f));
}

如上所示,如果是Fixed,则直接返回一个LayoutUnit封装的数据,1px = 1 << 6 = 64 unit,这也是Blink存储的精度。从这里可以看到,设置小数的px其实是有用的。

如果是Percent百分比,则用百分比乘以最大值,而这个最大值是用容器传进来的宽度。

(2)计算margin值

上面的div的margin给它设置了margin: 0 auto,需要计算实际的数字。blink会检测两边是不是都为auto,如果是的话就认为是居中:

// CSS 2.1: "If both 'margin-left' and 'margin-right' are 'auto', their used
// values are equal. This horizontally centers the element with respect to
// the edges of the containing block."
const ComputedStyle& containingBlockStyle = containingBlock->styleRef();
if (marginStartLength.isAuto() && marginEndLength.isAuto()) {
  LayoutUnit centeredMarginBoxStart = std::max(
      LayoutUnit(),
      (availableWidth - childWidth) / 2); 
  marginStart = centeredMarginBoxStart;
  marginEnd = availableWidth - childWidth - marginStart;
  return;
}

上面第8行用容器的宽度减掉本身的宽度,然后除以2就得到margin-left,接着用容器的宽度减掉本身的宽度和margin-left就得到margin-right。为什么margin-right还要再算一下,因为上面的代码是删减版的,它还有另外一种情况要处理,这里不是很重要,被我省掉了。

margin和width算好了,便把它放到layoutObject结点的盒模型数据结构里面:

m_frameRect.setWidth(width);
m_marginBoxOutsets.setStart(marginLeft);

(3)盒模型数据结构

在blink的源码注释里面,很形象地画出了盒模型图:

// ***** THE BOX MODEL *****
// The CSS box model is based on a series of nested boxes:
// http://www.w3.org/TR/CSS21/box.html
//
//       |----------------------------------------------------|
//       |                                                    |
//       |                   margin-top                       |
//       |                                                    |
//       |     |-----------------------------------------|    |
//       |     |                                         |    |
//       |     |             border-top                  |    |
//       |     |                                         |    |
//       |     |    |--------------------------|----|    |    |
//       |     |    |                          |    |    |    |
//       |     |    |       padding-top        |####|    |    |
//       |     |    |                          |####|    |    |
//       |     |    |    |----------------|    |####|    |    |
//       |     |    |    |                |    |    |    |    |
//       | ML  | BL | PL |  content box   | PR | SW | BR | MR |
//       |     |    |    |                |    |    |    |    |
//       |     |    |    |----------------|    |    |    |    |
//       |     |    |                          |    |    |    |
//       |     |    |      padding-bottom      |    |    |    |
//       |     |    |--------------------------|----|    |    |
//       |     |    |                      ####|    |    |    |
//       |     |    |     scrollbar height ####| SC |    |    |
//       |     |    |                      ####|    |    |    |
//       |     |    |-------------------------------|    |    |
//       |     |                                         |    |
//       |     |           border-bottom                 |    |
//       |     |                                         |    |
//       |     |-----------------------------------------|    |
//       |                                                    |
//       |                 margin-bottom                      |
//       |                                                    |
//       |----------------------------------------------------|
//
// BL = border-left
// BR = border-right
// ML = margin-left
// MR = margin-right
// PL = padding-left
// PR = padding-right
// SC = scroll corner (contains UI for resizing (see the 'resize' property)
// SW = scrollbar width

上面的盒模型耳熟能详,不太一样的是,它还把滚动条给画出来了。

这个盒模型border及其以内区域是用一个LayoutRect m_frameRect对象表示的:

// The CSS border box rect for this box.
//
// The rectangle is in this box's physical coordinates.
// The location is the distance from this
// object's border edge to the container's border edge (which is not
// always the parent). Thus it includes any logical top/left along
// with this box's margins.
LayoutRect m_frameRect;

上面源码注释说得很明白,意思是说这个LayoutRect的位置是从它本身的边到容器的边的距离,因此它的距离/位置包含了margin值和left/top的位移偏差。LayoutRect记录了一个盒子的位置和大小:

LayoutPoint m_location;
  LayoutSize m_size;

上面(1)和(2)计算好宽度后就去设置这个大小,保存起来。

可以在源码里面看到用这个对象对处理的一些获取宽度的方式,如clientWidth:

// More IE extensions.  clientWidth and clientHeight represent the interior of
// an object excluding border and scrollbar.
LayoutUnit LayoutBox::clientWidth() const {
  return m_frameRect.width() - borderLeft() - borderRight() -
         verticalScrollbarWidth();
}

clientWidth是除去border和scrollbar的宽度。

而offsetWidth是frameRect的宽度——算上border和scrollbar:

// IE extensions. Used to calculate offsetWidth/Height.
LayoutUnit offsetWidth() const override { return m_frameRect.width(); }
LayoutUnit offsetHeight() const override { return m_frameRect.height(); }

Margin区域是用一个LayoutRectOutsets表示的,这个对象记录了margin的上下左右值:

LayoutUnit m_top;
LayoutUnit m_right;
LayoutUnit m_bottom;
LayoutUnit m_left;

上面已经分析宽高的计算,还差位置的计算。

(4)位置计算

位置计算就是要算出x和y或者说left和top的值,这两个值分别在下面两个函数计算得到:

// Now determine the correct ypos based off examination of collapsing margin
// values.
LayoutUnit logicalTopBeforeClear =
    collapseMargins(child, layoutInfo, childIsSelfCollapsing,
                    childDiscardMarginBefore, childDiscardMarginAfter);
// Now place the child in the correct left position
determineLogicalLeftPositionForChild(child);

用以下html做为例子:

<!DOCType html>
<html>
<head></head>
<body>
    <div id="div-1" style="border:5px solid #000; width:50%; height: 100px; margin: 0 auto;"></div>
    <div id="div-2" style="margin: 50px; padding:80px; border: 20px solid">
        <div id="div-3" style="margin:15px">hello, world</div>
    </div>
</body>
</html>

我先把计算出来的结果打印出来,如下所示:

[LayoutBlockFlow.cpp(925)] location is: “190.25”, “0” size is “400.5”, “110”  (div-1)

[LayoutBlockFlow.cpp(925)] location is: “115”, “115” size is “451”, “18”           (div-3)

[LayoutBlockFlow.cpp(925)] location is: “50”, “160” size is “681”, “248”        (div-2)

[LayoutBlockFlow.cpp(925)] location is: “8”, “8” size is “781”, “408”               (body)

[LayoutBlockFlow.cpp(925)] location is: “0”, “0” size is “797”, “466”               (html)

 

由于它是一个递归的过程,所以上面打印的顺序是由子元素到父元素的。以div-2为例算一下,它的x = 50, y = 160:因为div-1占据的空间为h = border * 2 + height = 5 * 2 + 100 = 110,并且div-2有一个margin-top = 50,所以div-2的y = 110 + 50 = 160.

对于div-3,由于div-2有一个80px的padding和20px的border,同时它自己本身有一个15px的margin,所以div-3的y = 50 + 20 + 15 = 115.

如果把行内元素也打印出来,那么结果是这样的:

[LayoutBlockFlowLine.cpp(1997)] inline location is: “0”, “0” size is “400.5”, “10”  (div-1 content)

[LayoutBlockFlow.cpp(925)] location is: “190.25”, “0” size is “400.5”, “110”

[LayoutBlockFlowLine.cpp(1997)] inline location is: “0”, “115” size is “451”, “18”    (div-3 text)

[LayoutBlockFlow.cpp(925)] location is: “115”, “115” size is “451”, “18”

…(后面一样)

第三行是div-3的文本节点创建的layoutObject,它的行高是18px,所以它的size高度是18px。

这里可以看到块级元素间的空白节点不会产生layoutObject,这在代码里面可以找到佐证:

bool Text::textLayoutObjectIsNeeded(const ComputedStyle& style,
                                    const LayoutObject& parent) const {
  if (!length())
    return false;
  if (style.display() == EDisplay::None)
    return false;
  if (!containsOnlyWhitespace())
    return true;

  //其它判断
}

上面代码第7行,如果Text结点含有非空白字符,则马上返回true,否则的话继续判断:

    if (parent.isLayoutBlock() && !parent.childrenInline() &&
        (!prev || !prev->isInline()))
      return false;

第二行——如果存在上一个相邻结点,并且这个结点不是行内元素则返回false,不创建layout对象。

所以在块级元素后面的空白文本结点将不会参与渲染,这个就解释了为什么块级元素后的换行不会被转换成一个空格。在源码里面还可以看到,块级元素内的开头空白字符将会被忽略:

// Whitespace at the start of a block just goes away.  Don't even
// make a layout object for this text.

这里有个问题,为什么它要递归地算,即先算子元素的再回过头来算父元素呢?因为有些属性必须得先知道子元素的才能知道父元素,例如父元素的高度是子元素撑起的,但是有些属性要先知道父元素的才能算子元素的,例如子元素的宽度是父元素的50%。所以在计算子元素之前会先把当前元素的layout计算一下,然后再传给子元素,子元素计算好之后会返回父元素是否需要重新layout,如下:

  // Use the estimated block position and lay out the child if needed. After
  // child layout, when we have enough information to perform proper margin
  // collapsing, float clearing and pagination, we may have to reposition and
  // lay out again if the estimate was wrong.
  bool childNeededLayout =
      positionAndLayoutOnceIfNeeded(child, logicalTopEstimate, layoutInfo);

具体的计算过程,这里举一两个例子,例如计算left值时,会先取父元素的border-left和padding-left作为起始位置,然后再加上它自己的margin-left就得到它的x/left值。

void LayoutBlockFlow::determineLogicalLeftPositionForChild(LayoutBox& child) {
  LayoutUnit startPosition = borderStart() + paddingStart();
  LayoutUnit initialStartPosition = startPosition;

  LayoutUnit childMarginStart = marginStartForChild(child);
  LayoutUnit newPosition = startPosition + childMarginStart;
  //other code
}

我们知道浮动的规则比较复杂,所以相应的计算也比较复杂,我们简单研究一下。

(5)浮动

用以下三栏布局作为说明:

<div>
    <div style="float:left">hello, world</div>
    <div style="float:right"><p style="width:100px"></p></div>
    <div style="margin:0 100px;"></div>
</div>

先来看宽度的计算,对于第一个float: left的div,首先它会先判断一下宽度是否需要fit content:

bool LayoutBox::sizesLogicalWidthToFitContent(
    const Length& logicalWidth) const {
  if (isFloating() || isInlineBlockOrInlineTable())
    return true;
  //other code
}

如果它是浮动的或者是inlne-block,则需要宽度适应内容。由于子元素是一


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论