溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么從Chrome源碼看瀏覽器進(jìn)行l(wèi)ayout布局

發(fā)布時間:2021-11-16 17:15:04 來源:億速云 閱讀:112 作者:柒染 欄目:web開發(fā)

本篇文章為大家展示了怎么從Chrome源碼看瀏覽器進(jìn)行l(wèi)ayout布局,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。

假設(shè)有以下html/css:

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

怎么從Chrome源碼看瀏覽器進(jìn)行l(wèi)ayout布局

這在瀏覽器上面將顯示一個框:

怎么從Chrome源碼看瀏覽器進(jìn)行l(wèi)ayout布局

為了畫出這個框,首先要知道從哪里開始畫、畫多大,其次是邊緣stroke的顏色,就可以把它畫出來了:

void draw(SkCanvas* canvas) {
    SkPaint paint;
    paint.setStrokeWidth(1);//從位置為(200, 200)的地方開始畫,寬度為400,高度為100SkRect rect = SkRect::MakeXYWH(200, 200, 400, 100);
    canvas->drawRect(rect, paint);
}

上面是用Skia畫的代碼,Skia是一個跨平臺的開源2D圖形庫,是Chrome/firefox/android采用的底層Paint引擎。

為了能夠獲取到具體的值,就得進(jìn)行l(wèi)ayout。什么叫l(wèi)ayout?把css轉(zhuǎn)化成維度位置等可直接用來描繪的信息的過程就叫l(wèi)ayout,如下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轉(zhuǎn)化成ComputedStyle,上面的div,它被轉(zhuǎn)化后的style如下所示:

怎么從Chrome源碼看瀏覽器進(jìn)行l(wèi)ayout布局

width的大小是50,類型是百分比,而margin值是0,類型是auto,這兩種都不能直接用來畫的。所以需要通過layout計算出具體的數(shù)字。

 1. 建立layout樹

《從Chrome源碼看瀏覽器如何構(gòu)建DOM樹》這篇文章介紹了如何html文本的過程。當(dāng)解析完收到的html片段后,會觸發(fā)Layout Tree的構(gòu)建:

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

每個非display:none/content的Node結(jié)點都會相應(yīng)地創(chuàng)建一個LayoutObject,如下blink源碼的注釋:

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

并建立起它們的父子兄弟關(guān)系:

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

形成一棵獨立的layout樹。

當(dāng)layout樹建立好之后,緊接著用style計算layout的值。

2. 計算layout值

以上面的div為例,它需要計算它的寬度和margin。

(1)計算寬度

寬度的計算是根據(jù)數(shù)值的類型:

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封裝的數(shù)據(jù),1px = 1 << 6 = 64 unit,這也是Blink存儲的精度。從這里可以看到,設(shè)置小數(shù)的px其實是有用的。

如果是Percent百分比,則用百分比乘以***值,而這個***值是用容器傳進(jìn)來的寬度。

(2)計算margin值

上面的div的margin給它設(shè)置了margin: 0 auto,需要計算實際的數(shù)字。blink會檢測兩邊是不是都為auto,如果是的話就認(rèn)為是居中:

// 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結(jié)點的盒模型數(shù)據(jù)結(jié)構(gòu)里面:

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

(3)盒模型數(shù)據(jù)結(jié)構(gòu)

在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及其以內(nèi)區(qū)域是用一個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記錄了一個盒子的位置和大?。?/p>

LayoutPoint m_location;  LayoutSize m_size;

上面(1)和(2)計算好寬度后就去設(shè)置這個大小,保存起來。

可以在源碼里面看到用這個對象對處理的一些獲取寬度的方式,如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的寬度&mdash;&mdash;算上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區(qū)域是用一個LayoutRectOutsets表示的,這個對象記錄了margin的上下左右值:

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

上面已經(jīng)分析寬高的計算,還差位置的計算。

(4)位置計算

位置計算就是要算出x和y或者說left和top的值,這兩個值分別在下面兩個函數(shù)計算得到:

// 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 positiondetermineLogicalLeftPositionForChild(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>

我先把計算出來的結(jié)果打印出來,如下所示:

[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占據(jù)的空間為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.

如果把行內(nèi)元素也打印出來,那么結(jié)果是這樣的:

[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”

&hellip;(后面一樣)

第三行是div-3的文本節(jié)點創(chuàng)建的layoutObject,它的行高是18px,所以它的size高度是18px。

這里可以看到塊級元素間的空白節(jié)點不會產(chǎn)生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結(jié)點含有非空白字符,則馬上返回true,否則的話繼續(xù)判斷:

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

第二行&mdash;&mdash;如果存在上一個相鄰結(jié)點,并且這個結(jié)點不是行內(nèi)元素則返回false,不創(chuàng)建layout對象。

所以在塊級元素后面的空白文本結(jié)點將不會參與渲染,這個就解釋了為什么塊級元素后的換行不會被轉(zhuǎn)換成一個空格。在源碼里面還可以看到,塊級元素內(nèi)的開頭空白字符將會被忽略:

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

這里有個問題,為什么它要遞歸地算,即先算子元素的再回過頭來算父元素呢?因為有些屬性必須得先知道子元素的才能知道父元素,例如父元素的高度是子元素?fù)纹鸬?,但是有些屬性要先知道父元素的才能算子元素的,例如子元素的寬度是父元素?0%。所以在計算子元素之前會先把當(dāng)前元素的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}

我們知道浮動的規(guī)則比較復(fù)雜,所以相應(yīng)的計算也比較復(fù)雜,我們簡單研究一下。

(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,則需要寬度適應(yīng)內(nèi)容。由于子元素是一個行內(nèi)文本,它需要計算這個行內(nèi)元素的寬度,計算的規(guī)則非常復(fù)雜,這里我把部分注釋說明貼出來:

// (3) A text object. Text runs can have breakable characters at the//     start, the middle or the end. They may also lose whitespace off the//     front if we're already ignoring whitespace. In order to compute//     accurate min-width information, we need three pieces of//     information.//     (a) the min-width of the first non-breakable run. Should be 0 if//         the text string starts with whitespace.//     (b) the min-width of the last non-breakable run. Should be 0 if the//         text string ends with whitespace.//     (c) the min/max width of the string (trimmed for whitespace).

第二個浮動的div,它的子元素是一個p標(biāo)簽,并且它已經(jīng)指定了寬度。它會去算子元素的寬度加上margin值的寬度,還要判斷是否為浮動,循環(huán)所有子元素處理,取一個***值。

再來看位置的計算,計算位置的代碼還是能夠稍微看出點苗頭,例如對float: left的計算:

//如果當(dāng)前行的剩余空間小于float的寬度,則循環(huán)條件成立while (logicalRightOffsetForPositioningFloat(
           logicalTopOffset, logicalRightOffset, &heightRemainingRight) -           floatLogicalLeft <       floatLogicalWidth) {
  //往下挪
  logicalTopOffset +=
      std::min<LayoutUnit>(heightRemainingLeft, heightRemainingRight);
  //計算新的float left位置  floatLogicalLeft = logicalLeftOffsetForPositioningFloat(
      logicalTopOffset, logicalLeftOffset, &heightRemainingLeft);
  }
}
//循環(huán)結(jié)束,找到位置floatLogicalLeft = std::max(
    logicalLeftOffset - borderAndPaddingLogicalLeft(), floatLogicalLeft);

上面它會先判斷當(dāng)前行剩余空間是否小于浮動元素的寬度,如果是的話就一直往下挪。

通過上面的層層計算,就可以拿到位置坐標(biāo)和具體大小,上面兩個浮動的div***計算的結(jié)果是:

[LayoutBlockFlow.cpp(1475)] location is: “0”, “0” size is “77.3281”, “18”

[LayoutBlockFlow.cpp(1475)] location is: “681”, “0” size is “100”, “16”

有了這些信息,結(jié)合顏色等style,就可進(jìn)行Paint了。

3. Paint

Paint又是一塊很塊很復(fù)雜的東西,試圖在一篇文章里面講明layout都已經(jīng)是一件不太可能的事情。

Paint的初始化會使用layout的數(shù)據(jù),如下面的BoxPainter的構(gòu)造函數(shù):

BoxPainter(const LayoutBox& layoutBox) : m_layoutBox(layoutBox) {}

Paint會調(diào)用最上面說的Skia的SkCanvas畫:

SkCanvas* canvas() { return m_canvas; }

這個SkCanvas和JS里面的canvas有什么聯(lián)系和區(qū)別?

Blink JS里的canvas就是這個canvas,當(dāng)在js里面獲取canvas對象進(jìn)行描繪時:

var canvas = document.getElementById("canvas"); 
var ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(10, 10, 100, 100)

就會去獲取SkCanvas實例。

SkCanvas* HTMLCanvasElement::drawingCanvas() const {  return buffer() ? m_imageBuffer->canvas() : nullptr;
}

所以不管是用html/css畫,還是用canvas畫,它們都是同宗同源的,區(qū)別就在于借助html/css比較直觀簡單,瀏覽器幫你進(jìn)行l(wèi)ayout。而直接用canvas就得從點線面一點一點地去畫,但同時它的靈活度就比較大。

4. 觸發(fā)layout

什么時候會觸發(fā)layout,上文的分析已經(jīng)提及當(dāng)html片段解析完會觸發(fā)layout。在上一篇也有提及加載完css后也會觸發(fā)layout,同時resize頁面的時候也會觸發(fā)layout。因為layout的計算是比較復(fù)雜的,所以應(yīng)減少layout的次數(shù)。例如CSS要寫在head標(biāo)簽里面,不然寫在body里面,一旦遇到新的CSS又會重新layout。

第二個是獲取clientWidth/scrollTop等維度信息時,普遍的說法是會觸發(fā)layout用于獲取值,但是在筆者的觀察下并沒有觸發(fā)layout:
如下使用getComputedStyle或者獲取clientWidth應(yīng)該會觸發(fā)layout:

var style = window.getComputedStyle(document.getElementById("body"));var width = style.width;console.log(document.getElementById("canvas").clientWidth)

但是無論是我打斷點還是打log,都無法觀察到layout的觸發(fā),而是直接去獲取clientWidth的值:

LayoutUnit LayoutBox::clientWidth() const {  return m_frameRect.width() - borderLeft() - borderRight() -
         verticalScrollbarWidth();
}

不過,改變它的clientWidth的時候是一定會觸發(fā)layout的:

document.getElementById("canvas").style.width = "500px";

在layout的函數(shù)里面打印的Log:

怎么從Chrome源碼看瀏覽器進(jìn)行l(wèi)ayout布局

上面幾行是重新計算CSS,下面幾行是進(jìn)行l(wèi)ayout。

另外需要注意的是盡可能地減少layout的范圍,如下的demo&mdash;&mdash;當(dāng)點擊菜單按鈕的時候把菜單給放出來:

<style>     #menu,      #show-btn{         display: none;     }     .show-menu #menu,     .show-menu #show-btn{         display: block;     } </style><body><span id="show-btn">Menu</span><nav id="menu"><ul><li></li></ul></nav></body><script>     document.getElementById("menu").onclick = function(){         document.body.addClass("show-menu");     }; </script>

上面為了圖方便,給body添加了一個類,用這個類控制菜單的狀態(tài)。但是這樣會有很大的問題,因為給body添加了一個類導(dǎo)致它要重新計算style和layout,它一旦layout了,它的子元素也要跟著layout,也就是說整個頁面都要重新layout。所以這個代價就很高了,我們應(yīng)該縮小影響范圍。因此,把show-menu的class加到直接相關(guān)的元素上面就好了。

至此,整一個頁面渲染過程就介紹完畢了。我們從html -> CSS -> layout ->  paint一步步分析了其中的過程,雖然介紹得不是很全面,但已經(jīng)把核心的過程剖析了一遍。由于寫html/css很多東西都是不透明,完全不知道背后是怎么工作的,只能是看文檔說這個標(biāo)簽是怎么用的,那個屬性會有什么效果,然后在瀏覽器上面看效果,有點任瀏覽器宰割的感覺。所以這個源碼解讀就是為了能夠窺探瀏覽器背后工作原理,這樣對寫代碼會有幫助,能夠做到心中有數(shù)。當(dāng)遇到一些比較困難的問題時,能夠很快的找到解決方案或者解決的方向。

例如筆者就遇到一個奇芭的問題,就是使用height: calc(100% - 80px)的時候,在手機(jī)Safari上面展開某個子菜單時,偶現(xiàn)菜單滑不動的情況。當(dāng)時就想很可能是在Safari在展開菜單時高度算錯了,導(dǎo)致overflow: auto不管用。所以在展開菜單后再手手動計算和設(shè)置height,然后就解決問題了。

上述內(nèi)容就是怎么從Chrome源碼看瀏覽器進(jìn)行l(wèi)ayout布局,你們學(xué)到知識或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識儲備,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI