[重拾CSS]一道面試題來看僞元素、包含塊和高度坍塌

前言

前幾天某個羣友在羣裏問了一道面試題,就是關於一個自適應的正方形佈局的困惑,先貼上代碼。我其實很長一段時間沒有寫 CSS 了,對於裏面的一些細節也比較模糊了,因此決定重拾 CSS,來重新捋一捋這題目中的一些知識點。(本文大多采用的講解方式爲 w3 的 CSS 標準 + MDN,如果對標準比較熟悉的大神請跳過這篇文章)

通過標準分析有什麼好處?最權威的解答,能夠少走彎路,不會出錯。

代碼:

<style>
.square {
    width: 30%;
    overflow: hidden;
    background: yellow;
}
.square::after {
    content: "";
    display: block;
    margin-top: 100%;
}
</style>
<div class="square"></div>

效果(https://codepen.io/hua1995116/pen/WNQyKBY):

image-20200517160937926

上面的實現看似簡單,但是我們去深究,大約會冒出以下三個問題。

  • ::after 僞元素有什麼特殊的魔法嗎?

  • margin-top:100%  爲什麼能夠自適應寬度?

  • overflow:hidden 在這裏是什麼作用?

因此我們會按照上述疑問來逐一講解。

本文所有 demo 都存放於 https://github.com/hua1995116/node-demo/tree/master/css-margin

::after 僞元素有什麼特殊的魔法嗎?

::after

說到 ::after 那就需要說到僞元素,我們先來看看僞元素的定義吧。

僞元素(Pseudo elements)表示文檔的抽象元素,超出了文檔語言明確創建的那些元素。

—— https://www.w3.org/TR/css-pseudo-4/#intro

再來看看 ::after 的特性:

When their computed content value is not none, these pseudo-elements generate boxes as if they were immediate children of their originating element, and can be styled exactly like any normal document-sourced element in the document tree.

根據描述來看,當僞元素 content 不爲 none 的時候,我們可以把他們當做正常的元素一樣來看待。

因此我們的示例轉化爲更加通俗易懂的樣子。

<style>
.square1 {
  width: 30%;
  background: red;
  overflow: hidden;
}
.square1-after {
  margin-top:100%;
}
</style>
<div class="square1">
  <div class="square1-after"></div>
</div>

既然說到了僞元素,我們也順便回顧一下僞類(Pseudo classes),他們的語法非常類似,卻是水和水銀的關係。

僞類

是添加到選擇器的關鍵字,指定要選擇的元素的特殊狀態。

: 單冒號開頭的爲僞類,代表形態爲 :hover

/* 所有用戶指針懸停的按鈕 */
button:hover {
  color: blue;
}

僞元素

表示文檔的抽象元素,超出了文檔語言明確創建的那些元素。(因爲它們並不侷限於適應文檔樹,所以可以使用它們來選擇和樣式化文檔中不一定映射到文檔樹結構的部分。)

:: 雙冒號開頭的爲僞元素,代表形態爲 ::after

僞元素是應用於元素

/* 每一個 <p> 元素的第一行。 */
p::first-line {
  color: blue;
  text-transform: uppercase;
}

僞類和僞元素區別

通過上述,我們可能大致理解了但是對於一些看上去用途很相似的僞類和僞元素還是有點迷糊。

「關鍵區別: 是否創建了一個超出文檔樹以外的元素。」

我們乍一看 ::first-line 的效果不是和 :first-child 一樣嘛?來舉一個例子。

// pseudo-elements.html
<style>
        p::first-line {
            color: blue;
        }
</style>
<div>
    <p>
        This is a somewhat long HTML
        paragraph that will be broken into several
        lines. 
    </p>
</div>
// pseudo-classes.html
<style>
        p:first-child {
            color: blue;
        }
</style>
<div>
    <p>
        This is a somewhat long HTML
        paragraph that will be broken into several
        lines.
    </p>
</div>
image-20200517185343290

像我剛纔所說乍一看,兩種效果是一樣的。但是他們真的一樣麼?當然不是!我們給他們加上寬度。

p {
 width: 200px;
}

::first-line的形態

image-20200517185652868

在真實的渲染中我們可以理解爲

<p><p::first-line>This is a somewhat long</p::first-line> HTML paragraph that will be broken into several lines.</p>

但是我們在真實的 DOM Tree 是看不到的。這一點規範中也說明了,因爲它們並不單單適用於文檔樹,所以使用它們來選擇和樣式化文檔不一定映射到文檔樹。

Since they are not restricted to fitting into the document tree, they can be used to select and style portions of the document that do not necessarily map to the document’s tree structure.

:first-child的形態

image-20200517185714812

小結

至此我們搞清楚了我們的第一個問題, ::after沒有魔法,在本題可以將它當成正常的元素,並且我們搞清楚了僞元素和僞類的區別。

margin-top:100%  爲什麼能夠自適應寬度?

現在我們已經將這個示例轉化成一個比較簡單的形態,沒有過多的知識。

<style>
.square1 {
  width: 30%;
  background: red;
  overflow: hidden;
}
.square1-after {
  margin-top:100%;
}
</style>
<div class="square1">
  <div class="square1-after"></div>
</div>

然後我們來看看這個margin-top: 100%,看上去他相對於了父元素的 width 值來進行計算的。那麼我們來看看 margin-top 到底是怎麼計算的。

https://www.w3.org/TR/CSS22/box.html#margin-properties

image-20200517192743042

可以看到 margin-top 主要有三種形態。第一種是固定值,第二種爲百分比,第三種爲 auto,這裏我們主要來看下 百分比的計算。

通過上述的描述,可以知道margin-top margin-bottom margin-left margin-right 百分比的長度是由當前元素的包含塊的寬度來決定的。

包含塊(Containing blocks)

那麼什麼是包含塊(Containing blocks)呢?

The position and size of an element's box(es) are sometimes calculated relative to a certain rectangle, called the containing block of the element.

—— https://www.w3.org/TR/CSS22/visudet.html#containing-block-details

元素盒子的位置和大小有時是相對於某個矩形計算的,稱爲元素的包含塊。

上述的描述有點拗口,我們大致只需要知道它就是一個矩形的塊。下面重要的來了,包含塊是怎麼確定的?(https://developer.mozilla.org/zh-CN/docs/Web/CSS/All_About_The_Containing_Block)

確定一個元素的包含塊的過程完全依賴於這個元素的 position 屬性:

  1. 如果 position 屬性爲 staticrelativesticky,包含塊可能由它的最近的祖先「塊元素」(比如說inline-block, block 或 list-item元素)的內容區的邊緣組成,也可能會建立格式化上下文(比如說 a table container, flex container, grid container, 或者是 the block container 自身)。

  2. 如果 position 屬性爲 absolute ,包含塊就是由它的最近的 position 的值不是 static (也就是值爲fixed, absolute, relativesticky)的祖先元素的內邊距區的邊緣組成。

  3. 如果 position 屬性是 fixed,在連續媒體的情況下(continuous media)包含塊是 viewport ,在分頁媒體(paged media)下的情況下包含塊是分頁區域(page area)。

  4. 如果 position 屬性是absolute 或fixed,包含塊也可能是由滿足以下條件的最近父級元素的內邊距區的邊緣組成的:

    1. A transform or perspective value other than none

    2. A will-change value of transform or perspective

    3. A filter value other than none or a will-change value of filter(only works on Firefox).

    4. A contain value of paint (例如: contain: paint;)

注意,以下所有例子的視口寬度都爲 594px

Case1

第一種情況,就是我們的例子的情況,當前元素的 position 沒有填寫,默認爲 static 。因此滿足第一種情況,取它最近的祖先元素,也就是包含塊爲 container.

<style>
.container {
  width: 30%;
}
.inner {
  margin-top:100%;
}
</style>
<div class="container">
  <div class="inner"></div>
</div>

因此inner「margin-top」 = 父元素container = 窗口寬度(594px) * 30% = 178.188px。

Case2

當前元素爲 position:absolute, 因此獲取的最近的一個 positionstatic 的元素

<style>
.outer {
    width: 500px;
   position: relative;
}
.container {
    width: 30%;
}
.inner {
    position: absolute;
    margin-top: 100%;
}
</style>
<div class="outer">
  <div class="container">
    <div class="inner"></div>
  </div>
</div>

這個時候inner「margin-top」 = outer 的寬度(500px)* 100% = 500px。

Case3

當前元素爲 position:fixed  ,此時的包含塊爲視口。

<style>
.outer {
    width: 500px;
   position: relative;
}
.container {
    width: 30%;
}
.inner {
    position: absolute;
    margin-top: 100%;
}
</style>
<div class="outer">
  <div class="container">
    <div class="inner"></div>
  </div>
</div>

因此這個時候 「margin-top」 = viewport 的寬度(594px)* 100% = 594px。此時是無關父元素,以及無關外層position 的設置的。

Case4

在 case2 和 case 3 的基礎上,會有一些特例影響包含塊的尋找。主要就以下4種情況

  1. A transform or perspective value other than none

  2. A will-change value of transform or perspective

  3. A filter value other than none or a will-change value of filter(only works on Firefox).

  4. A contain value of paint (例如: contain: paint;)

我舉一個 transform 例子來講解。

<style>
.outer {
    width: 500px;
   position: relative;
}
.container {
    width: 30%;
   transform: translate(0, 0);
}
.inner {
    position: fixed;
    margin-top: 100%;
}
</style>
<div class="outer">
  <div class="container">
    <div class="inner"></div>
  </div>
</div>

這個時候我們的計算又發生了變化,此時包含塊又變成了 container .

「margin-top」 = 父元素container = 窗口寬度(594px) * 30% = 178.188px。

小結

所以對於我們一開始的問題,就是我們的 Case1,採取的就是最近的父元素。所以 margin-top 就是 父元素 square1 的寬度,因此實現了一個自適應的正方形。

對於 position 的不同形態,對於佈局狀態的影響,一般在我們入門 css 的時候就學了,但是可能沒有那麼仔細去了解每種情況,也可能不知道他的名詞,叫做包含塊,這次我們對它進行了梳理,這一節就這樣結束,繼續看!

overflow:hidden 在這裏是什麼作用?

假如我們把 overflow:hidden 去了。

<style>
.square1 {
  width: 30%;
  background: red;
}
.square1-after {
  margin-top:100%;
}
</style>
<div class="square1">
  <div class="square1-after"></div>
</div>

我們可以看到以上執行完顯示出現的畫面爲一篇空白。此時我們就要引出了我們的最後一個概念就是,「邊距坍塌(Collapsing margins)」 .

邊距塌陷(Collapsing margins)

在CSS中,兩個或多個框(可能是也可能不是兄弟)的相鄰邊距可以合併形成一個邊距,稱爲邊距塌陷。

不會發生邊距坍塌的情況

  • 根節點元素

  • 水平邊距(Horizontal margins)不會崩潰

  • 「如果具有間隙的元素的頂部和底部相鄰,他會與後續同級的元素邊距一起坍塌,但是不會與父元素底部的一起坍塌(If the top and bottom margins of an element with clearance are adjoining, its margins collapse with the adjoining margins of following siblings but that resulting margin does not collapse with the bottom margin of the parent block.)」

  • 父子元素,父元素有非0的 min-height且有autoheight,父子元素都含有 margin-bottom,此時 margin-bottom 不會發生邊距坍塌。

  • 在不同BFC(塊級格式上下文)

對於以上,可能對於「情況3」「情況4」會比較疑惑,所以舉例子如下。

case3
<style>
.case {
            width: 200px;
            background-color: yellow;
        }

        .container {
            background-color: lightblue;
            margin-bottom: 70px;
            padding-top: 0.01px;
        }

        .preface {
            float: left;
            height: 58px;
            width: 100px;
            border: 1px solid red;
        }

        .one .intro {
            clear: left;
            margin-top: 60px;
        }

        .two .intro {
            clear: left;
            margin-top: 59px;
            margin-bottom: 20px;
        }
</style>
<div class="case one">
        <div class="container">
            <div class="preface">
                lorem ipsum
            </div>
            <div class="intro"></div>
        </div>
        after
    </div>
    <hr>
    <div class="case two">
        <div class="container">
            <div class="preface">
                lorem ipsum
            </div>
            <div class="intro"></div>
        </div>
        after
    </div>

在 Firefox 和 IE 下的效果(谷歌失效,原因可能和谷歌瀏覽器實現有關,暫未深追。)

image-20200519203941769

可以看到如果在在沒有 clearance 的情況下,父元素底部是會隨着子元素一起坍塌的,但是如果中間有 clearance 的情況下,父元素的底部則不會坍塌。

case4
<style>
  .case2 {
    min-height: 200px;
    height: auto;
    background: red;
    margin-bottom: 20px;
  }

  .case2-inner {
    margin-bottom: 50px;
  }
</style>
<div class="case2">
  <div class="case2-inner"></div>
</div>
<div>爲了看間距效果</div>

效果:

image-20200518001513036

可以看到這種情況下,父子元素下邊距並不會發生邊距坍塌。

會發生邊距坍塌

發生邊距坍塌需要滿足2個前提

1.是 block 盒子模型,在同一個 BFC。

2.兩個元素之間沒有行內元素,沒有 clearance  ,沒有 padding,沒有border。

然後以下幾種情況會發生邊距坍塌。

  • 盒子的上邊距和第一個流入子元素的上邊距

  • 盒子的下邊距和同級後一個流入元素的上邊距

  • 如果父元素高度爲“auto”,最後一個流入子元素的底部距和其父元素的底部距

  • 某個元素沒有建立新的 BFC,並且 min-height 和 height 都爲 0,同時含有 margin-top 和 margin-bottom.

  • 「如果'min-height'屬性爲零,並且框沒有頂部或底部邊框,也沒有頂部或底部填充,並且框的'height'爲0或'auto',並且框不包含邊距,則框自身的邊距會摺疊 行框,其所有流入子頁邊距(如果有的話)都會崩潰。」

補充: 如果'min-height'屬性爲零,並且框沒有頂部或底部border,也沒有頂部或底部padding,並且元素的'height'爲0或'auto',並且沒有行內元素,則元素自身的所有邊距坍塌,包括其所有流入子元素的邊距(如果有的話)都會坍塌。

「這裏有幾個問題要解釋一下 1.什麼是流入子元素,2. 是什麼 clearance」

1.流入元素

流入元素需要用的反向來進行介紹,有流入元素,就有流出元素,以下情況爲流出元素。

  • floated items。浮動的元素

  • items with position: absolute (including position: fixed which acts in the same way)。通過設置position屬性爲absolute或者fixed的元素

  • the root element (html)根元素

除了以上情況的元素,叫做流入元素。

<style>
body {
border: 1px solid #000;
}

.case2 {
width: 200px;
height: 50px;
background: red;
}

.case2-inner {
margin-top: 50px;
height: 0;
}

.float {
float: left;
}
</style>
<div class="case2">
<div class="float"></div>
<div class="case2-inner">看出了啥</div>
</div>
image-20200519013902840
2.clearance

當某個元素有clear 非 none 值 並且盒子實際向下移動時,它叫做 clearance。

case1
<style>
.case1 {
            height: 50px;
            background: red;
            margin-top: 100px;
        }

        .case1-inner {
            margin-top: 50px;
        }
</style>
<div class="case1">
        <div class="case1-inner">我直接從頂部開始了</div>
    </div>
image-20200519001450483
case2
<style>
.case2 {
            height: 150px;
            background: red;
        }

        .case2-inner1 {
            margin-bottom: 50px;
        }

        .case2-inner2 {
            margin-top: 20px;
        }
</style>
<div class="case2">
        <div class="case2-inner1">我和底下之間距離爲50px</div>
        <div class="case2-inner2">我和頂上之間距離爲50px</div>
    </div>
image-20200519001526280
case3
<style>
.case3 {
            height: auto;
            background: red;
            margin-bottom: 10px;
        }

        .case3-inner {
            margin-bottom: 50px;
        }
</style>
<div class="case3">
        <div class="case3-inner">底部和父元素被合併了</div>
    </div>
    <div>距離頂上50px</div>
image-20200519001635407
case4
<style>
.case4 {
            height: 200px;
            background: red;
        }

        .case4-inner {
            margin-top: 20px;
            margin-bottom: 30px;
        }
</style>
<div class="case4">
        <p>它把自己給合併了,距離底下30px</p>
        <div class="case4-inner"><span style="clear: both;"></span></div>
        <p>它把自己給合併了, 距離頂上30px</p>
    </div>
image-20200519001704179

邊距塌陷如何解決

通用型

1.改變盒子模型(非 block 模型)

2.創建新的 BFC

限制型

查看剛纔不會發生高度坍塌的情況

邊距塌陷如何計算

1.當兩個或更多邊距坍塌時,當邊距全爲正數的時候,結果頁邊距寬度是邊距塌陷寬度的最大值。

2.當邊距全爲負數的時候,取最小值。

3.在存在負邊距的情況下,從正邊距的最大值中減去負邊距的絕對值的最大值。 (-13px 8px 100px疊在一起,則邊距塌陷的值爲 100px - 13px = 87px)

如果轉爲算法就是以下代碼

// AllList 所有坍塌邊距
function computed(AllList) {
  const PositiveList = AllList.filter((item) => item >= 0);
  const NegativeList = AllList.filter((item) => item <= 0);
  const AllPositive = AllList.every((item) => item >= 0);
  const AllNegative = AllList.every((item) => item <= 0);
  if (AllNegative) {
    return Math.min(...AllList);
  } else if (AllPositive) {
    return Math.max(...AllList);
  } else {
    const maxPositive = Math.max(...PositiveList);
    const minNegative = Math.min(...NegativeList);
    return maxPositive + minNegative;
  }
}

小結

通過上面對邊距坍塌的理解,我們可以很快得出,我們的自適應正方形中的例子,子元素的 margin-top 和 父元素的 margin-top 發生了坍塌,因此可以新建一個 BFC 來消除這個問題。而 overflow:hidden 就是會形成一個 新的 BFC 。BFC詳見 https://developer.mozilla.org/zh-CN/docs/Web/Guide/CSS/Block_formatting_context

總結

通過上面的解析,我們終於把這一道小小的面試題,進行了全方位的剖析。每一個問題都對應着一個知識塊。

  • ::after 僞元素有什麼特殊的魔法嗎?  ->  僞元素(Pseudo elements)

  • margin-top:100%  爲什麼能夠自適應寬度?  -> 包含塊 (Containing blocks)

  • overflow:hidden 在這裏是什麼作用?  -> 邊距塌陷(Collapsing margins)

想不到小小的面試題,居然可以牽扯出這麼多的知識,所以我們在面對一些面試題的時候,例如實現一個自適應的正方形佈局,別單單看有幾種方式能夠實現,解決方法永遠會隨着時間的推進,變得越來越多,那我們能做的就是以不變應萬變(當然規範也是相對的,也可能會變,只是概率低)去理解剖析這些方法背後的用到的知識。

相信如果你把以上搞懂了,面試官對你深層次的靈魂追問,你也能對答如流了。注意本文的一些專有名詞,我都用英文多次標註,這也許未來會對你有所幫助。

穩住,我們能贏!嘻嘻嘻,最後,如果你對題目的理解一時間比較迷茫,歡迎加羣提問,本文也是基於羣友的問題,展開了一系列的講解。

參考鏈接

https://stackoverflow.com/questions/21685648/what-exactly-is-clearance-in-css

https://www.w3.org/TR/css-display-3/#in-flow

https://stackoverflow.com/questions/25350805/margin-collapse-and-clearance

https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Flow_Layout/%E5%9C%A8Flow%E4%B8%AD%E5%92%8CFlow%E4%B9%8B%E5%A4%96

關注

歡迎關注公衆號 「「秋風的筆記」」,主要記錄日常中覺得有意思的工具以及分享開發實踐,保持深度和專注度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章