掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
假設(shè)有以下html/css:

創(chuàng)新互聯(lián)建站是一家集網(wǎng)站建設(shè),樺川企業(yè)網(wǎng)站建設(shè),樺川品牌網(wǎng)站建設(shè),網(wǎng)站定制,樺川網(wǎng)站建設(shè)報(bào)價(jià),網(wǎng)絡(luò)營(yíng)銷,網(wǎng)絡(luò)優(yōu)化,樺川網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強(qiáng)企業(yè)競(jìng)爭(zhēng)力。可充分滿足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時(shí)我們時(shí)刻保持專業(yè)、時(shí)尚、前沿,時(shí)刻以成就客戶成長(zhǎng)自我,堅(jiān)持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實(shí)用型網(wǎng)站。
這在瀏覽器上面將顯示一個(gè)框:
為了畫出這個(gè)框,首先要知道從哪里開(kāi)始畫、畫多大,其次是邊緣stroke的顏色,就可以把它畫出來(lái)了:
void draw(SkCanvas* canvas) {
SkPaint paint;
paint.setStrokeWidth(1);
//從位置為(200, 200)的地方開(kāi)始畫,寬度為400,高度為100
SkRect rect = SkRect::MakeXYWH(200, 200, 400, 100);
canvas->drawRect(rect, paint);
}上面是用Skia畫的代碼,Skia是一個(gè)跨平臺(tái)的開(kāi)源2D圖形庫(kù),是Chrome/firefox/android采用的底層Paint引擎。
為了能夠獲取到具體的值,就得進(jìn)行l(wèi)ayout。什么叫l(wèi)ayout?把css轉(zhuǎn)化成維度位置等可直接用來(lái)描繪的信息的過(guò)程就叫l(wèi)ayout,如下Chrome源碼對(duì)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源碼看瀏覽器如何計(jì)算CSS》這篇文章介紹了怎么把css轉(zhuǎn)化成ComputedStyle,上面的div,它被轉(zhuǎn)化后的style如下所示:
width的大小是50,類型是百分比,而margin值是0,類型是auto,這兩種都不能直接用來(lái)畫的。所以需要通過(guò)layout計(jì)算出具體的數(shù)字。
《從Chrome源碼看瀏覽器如何構(gòu)建DOM樹(shù)》這篇文章介紹了如何html文本的過(guò)程。當(dāng)解析完收到的html片段后,會(huì)觸發(fā)Layout Tree的構(gòu)建:
void Document::finishedParsing() {
updateStyleAndLayoutTree();
}每個(gè)非display:none/content的Node結(jié)點(diǎn)都會(huì)相應(yīng)地創(chuàng)建一個(gè)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);
形成一棵獨(dú)立的layout樹(shù)。
當(dāng)layout樹(shù)建立好之后,緊接著用style計(jì)算layout的值。
以上面的div為例,它需要計(jì)算它的寬度和margin。
寬度的計(jì)算是根據(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(maximumValue * length.percent() / 100.0f));
} 如上所示,如果是Fixed,則直接返回一個(gè)LayoutUnit封裝的數(shù)據(jù),1px = 1 << 6 = 64 unit,這也是Blink存儲(chǔ)的精度。從這里可以看到,設(shè)置小數(shù)的px其實(shí)是有用的。
如果是Percent百分比,則用百分比乘以***值,而這個(gè)***值是用容器傳進(jìn)來(lái)的寬度。
上面的div的margin給它設(shè)置了margin: 0 auto,需要計(jì)算實(shí)際的數(shù)字。blink會(huì)檢測(cè)兩邊是不是都為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還要再算一下,因?yàn)樯厦娴拇a是刪減版的,它還有另外一種情況要處理,這里不是很重要,被我省掉了。
margin和width算好了,便把它放到layoutObject結(jié)點(diǎn)的盒模型數(shù)據(jù)結(jié)構(gòu)里面:
m_frameRect.setWidth(width); m_marginBoxOutsets.setStart(marginLeft);
在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
上面的盒模型耳熟能詳,不太一樣的是,它還把滾動(dòng)條給畫出來(lái)了。
這個(gè)盒模型border及其以內(nèi)區(qū)域是用一個(gè)LayoutRect m_frameRect對(duì)象表示的:
// 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;
上面源碼注釋說(shuō)得很明白,意思是說(shuō)這個(gè)LayoutRect的位置是從它本身的邊到容器的邊的距離,因此它的距離/位置包含了margin值和left/top的位移偏差。LayoutRect記錄了一個(gè)盒子的位置和大?。?/p>
LayoutPoint m_location; LayoutSize m_size;
上面(1)和(2)計(jì)算好寬度后就去設(shè)置這個(gè)大小,保存起來(lái)。
可以在源碼里面看到用這個(gè)對(duì)象對(duì)處理的一些獲取寬度的方式,如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區(qū)域是用一個(gè)LayoutRectOutsets表示的,這個(gè)對(duì)象記錄了margin的上下左右值:
LayoutUnit m_top; LayoutUnit m_right; LayoutUnit m_bottom; LayoutUnit m_left;
上面已經(jīng)分析寬高的計(jì)算,還差位置的計(jì)算。
位置計(jì)算就是要算出x和y或者說(shuō)left和top的值,這兩個(gè)值分別在下面兩個(gè)函數(shù)計(jì)算得到:
// 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做為例子:
hello, world
我先把計(jì)算出來(lái)的結(jié)果打印出來(lái),如下所示:
[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)
由于它是一個(gè)遞歸的過(guò)程,所以上面打印的順序是由子元素到父元素的。以div-2為例算一下,它的x = 50, y = 160:因?yàn)閐iv-1占據(jù)的空間為h = border * 2 + height = 5 * 2 + 100 = 110,并且div-2有一個(gè)margin-top = 50,所以div-2的y = 110 + 50 = 160.
對(duì)于div-3,由于div-2有一個(gè)80px的padding和20px的border,同時(shí)它自己本身有一個(gè)15px的margin,所以div-3的y = 50 + 20 + 15 = 115.
如果把行內(nèi)元素也打印出來(lá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”
…(后面一樣)
第三行是div-3的文本節(jié)點(diǎn)創(chuàng)建的layoutObject,它的行高是18px,所以它的size高度是18px。
這里可以看到塊級(jí)元素間的空白節(jié)點(diǎn)不會(huì)產(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é)點(diǎn)含有非空白字符,則馬上返回true,否則的話繼續(xù)判斷:
if (parent.isLayoutBlock() && !parent.childrenInline() &&
(!prev || !prev->isInline()))
return false;第二行——如果存在上一個(gè)相鄰結(jié)點(diǎn),并且這個(gè)結(jié)點(diǎn)不是行內(nèi)元素則返回false,不創(chuàng)建layout對(duì)象。
所以在塊級(jí)元素后面的空白文本結(jié)點(diǎn)將不會(huì)參與渲染,這個(gè)就解釋了為什么塊級(jí)元素后的換行不會(huì)被轉(zhuǎn)換成一個(gè)空格。在源碼里面還可以看到,塊級(jí)元素內(nèi)的開(kāi)頭空白字符將會(huì)被忽略:
// Whitespace at the start of a block just goes away. Don't even // make a layout object for this text.
這里有個(gè)問(wèn)題,為什么它要遞歸地算,即先算子元素的再回過(guò)頭來(lái)算父元素呢?因?yàn)橛行傩员仨毜孟戎雷釉氐牟拍苤栏冈?,例如父元素的高度是子元素?fù)纹鸬?,但是有些屬性要先知道父元素的才能算子元素的,例如子元素的寬度是父元素?0%。所以在計(jì)算子元素之前會(huì)先把當(dāng)前元素的layout計(jì)算一下,然后再傳給子元素,子元素計(jì)算好之后會(huì)返回父元素是否需要重新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);具體的計(jì)算過(guò)程,這里舉一兩個(gè)例子,例如計(jì)算left值時(shí),會(huì)先取父元素的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
}我們知道浮動(dòng)的規(guī)則比較復(fù)雜,所以相應(yīng)的計(jì)算也比較復(fù)雜,我們簡(jiǎn)單研究一下。
用以下三欄布局作為說(shuō)明:
hello, world
先來(lái)看寬度的計(jì)算,對(duì)于***個(gè)float: left的div,首先它會(huì)先判斷一下寬度是否需要fit content:
bool LayoutBox::sizesLogicalWidthToFitContent(
const Length& logicalWidth) const {
if (isFloating() || isInlineBlockOrInlineTable())
return true;
//other code
}如果它是浮動(dòng)的或者是inlne-block,則需要寬度適應(yīng)內(nèi)容。由于子元素是一個(gè)行內(nèi)文本,它需要計(jì)算這個(gè)行內(nèi)元素的寬度,計(jì)算的規(guī)則非常復(fù)雜,這里我把部分注釋說(shuō)明貼出來(lái):
// (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).
第二個(gè)浮動(dòng)的div,它的子元素是一個(gè)p標(biāo)簽,并且它已經(jīng)指定了寬度。它會(huì)去算子元素的寬度加上margin值的寬度,還要判斷是否為浮動(dòng),循環(huán)所有子元素處理,取一個(gè)***值。
再來(lái)看位置的計(jì)算,計(jì)算位置的代碼還是能夠稍微看出點(diǎn)苗頭,例如對(duì)float: left的計(jì)算:
//如果當(dāng)前行的剩余空間小于float的寬度,則循環(huán)條件成立
while (logicalRightOffsetForPositioningFloat(
logicalTopOffset, logicalRightOffset, &heightRemainingRight) -
floatLogicalLeft <
floatLogicalWidth) {
//往下挪
logicalTopOffset +=
std::min(heightRemainingLeft, heightRemainingRight);
//計(jì)算新的float left位置
floatLogicalLeft = logicalLeftOffsetForPositioningFloat(
logicalTopOffset, logicalLeftOffset, &heightRemainingLeft);
}
}
//循環(huán)結(jié)束,找到位置
floatLogicalLeft = std::max(
logicalLeftOffset - borderAndPaddingLogicalLeft(), floatLogicalLeft); 上面它會(huì)先判斷當(dāng)前行剩余空間是否小于浮動(dòng)元素的寬度,如果是的話就一直往下挪。
通過(guò)上面的層層計(jì)算,就可以拿到位置坐標(biāo)和具體大小,上面兩個(gè)浮動(dòng)的div***計(jì)算的結(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了。
Paint又是一塊很塊很復(fù)雜的東西,試圖在一篇文章里面講明layout都已經(jīng)是一件不太可能的事情。
Paint的初始化會(huì)使用layout的數(shù)據(jù),如下面的BoxPainter的構(gòu)造函數(shù):
BoxPainter(const LayoutBox& layoutBox) : m_layoutBox(layoutBox) {}Paint會(huì)調(diào)用最上面說(shuō)的Skia的SkCanvas畫:
SkCanvas* canvas() { return m_canvas; }這個(gè)SkCanvas和JS里面的canvas有什么聯(lián)系和區(qū)別?
Blink JS里的canvas就是這個(gè)canvas,當(dāng)在js里面獲取canvas對(duì)象進(jìn)行描繪時(shí):
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(10, 10, 100, 100)就會(huì)去獲取SkCanvas實(shí)例。
SkCanvas* HTMLCanvasElement::drawingCanvas() const {
return buffer() ? m_imageBuffer->canvas() : nullptr;
}所以不管是用html/css畫,還是用canvas畫,它們都是同宗同源的,區(qū)別就在于借助html/css比較直觀簡(jiǎn)單,瀏覽器幫你進(jìn)行l(wèi)ayout。而直接用canvas就得從點(diǎn)線面一點(diǎn)一點(diǎn)地去畫,但同時(shí)它的靈活度就比較大。
什么時(shí)候會(huì)觸發(fā)layout,上文的分析已經(jīng)提及當(dāng)html片段解析完會(huì)觸發(fā)layout。在上一篇也有提及加載完css后也會(huì)觸發(fā)layout,同時(shí)resize頁(yè)面的時(shí)候也會(huì)觸發(fā)layout。因?yàn)閘ayout的計(jì)算是比較復(fù)雜的,所以應(yīng)減少layout的次數(shù)。例如CSS要寫在head標(biāo)簽里面,不然寫在body里面,一旦遇到新的CSS又會(huì)重新layout。
第二個(gè)是獲取clientWidth/scrollTop等維度信息時(shí),普遍的說(shuō)法是會(huì)觸發(fā)layout用于獲取值,但是在筆者的觀察下并沒(méi)有觸發(fā)layout:
如下使用getComputedStyle或者獲取clientWidth應(yīng)該會(huì)觸發(fā)layout:
var style = window.getComputedStyle(document.getElementById("body"));
var width = style.width;
console.log(document.getElementById("canvas").clientWidth)但是無(wú)論是我打斷點(diǎn)還是打log,都無(wú)法觀察到layout的觸發(fā),而是直接去獲取clientWidth的值:
LayoutUnit LayoutBox::clientWidth() const {
return m_frameRect.width() - borderLeft() - borderRight() -
verticalScrollbarWidth();
}不過(guò),改變它的clientWidth的時(shí)候是一定會(huì)觸發(fā)layout的:
document.getElementById("canvas").style.width = "500px";在layout的函數(shù)里面打印的Log:
上面幾行是重新計(jì)算CSS,下面幾行是進(jìn)行l(wèi)ayout。
另外需要注意的是盡可能地減少layout的范圍,如下的demo——當(dāng)點(diǎn)擊菜單按鈕的時(shí)候把菜單給放出來(lái):
Menu
上面為了圖方便,給body添加了一個(gè)類,用這個(gè)類控制菜單的狀態(tài)。但是這樣會(huì)有很大的問(wèn)題,因?yàn)榻obody添加了一個(gè)類導(dǎo)致它要重新計(jì)算style和layout,它一旦layout了,它的子元素也要跟著layout,也就是說(shuō)整個(gè)頁(yè)面都要重新layout。所以這個(gè)代價(jià)就很高了,我們應(yīng)該縮小影響范圍。因此,把show-menu的class加到直接相關(guān)的元素上面就好了。
至此,整一個(gè)頁(yè)面渲染過(guò)程就介紹完畢了。我們從html -> CSS -> layout -> paint一步步分析了其中的過(guò)程,雖然介紹得不是很全面,但已經(jīng)把核心的過(guò)程剖析了一遍。由于寫html/css很多東西都是不透明,完全不知道背后是怎么工作的,只能是看文檔說(shuō)這個(gè)標(biāo)簽是怎么用的,那個(gè)屬性會(huì)有什么效果,然后在瀏覽器上面看效果,有點(diǎn)任瀏覽器宰割的感覺(jué)。所以這個(gè)源碼解讀就是為了能夠窺探瀏覽器背后工作原理,這樣對(duì)寫代碼會(huì)有幫助,能夠做到心中有數(shù)。當(dāng)遇到一些比較困難的問(wèn)題時(shí),能夠很快的找到解決方案或者解決的方向。
例如筆者就遇到一個(gè)奇芭的問(wèn)題,就是使用height: calc(100% - 80px)的時(shí)候,在手機(jī)Safari上面展開(kāi)某個(gè)子菜單時(shí),偶現(xiàn)菜單滑不動(dòng)的情況。當(dāng)時(shí)就想很可能是在Safari在展開(kāi)菜單時(shí)高度算錯(cuò)了,導(dǎo)致overflow: auto不管用。所以在展開(kāi)菜單后再手手動(dòng)計(jì)算和設(shè)置height,然后就解決問(wèn)題了。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流