跳到主要內容
黯羽輕揚每天積累一點點

外邊距合併規則

免費2017-11-12#CSS#CSS margin collapse#collapsed margin#合并外边距#外边距折叠#CSS clearance

CSS 盒模型裡,很有意思但很難弄明白的一部分內容

寫在前面

margin 的合併規則算是 CSS 盒模型裡最複雜部分,沒有之一。因為這部分內容涉及很多不太容易理解的概念,例如 clearance(間隙)、normal flow/in-flow(常規流)、BFC(塊格式化上下文)、line box(行框)、inline box(行內框)、bidi(雙向環境)等等

CSS 盒模型不只是 7 項水平屬性 + 7 項垂直屬性:

margin
  border
    padding
      width/height

P.S. 想起高跟鞋的梗——「不僅有 padding,今天還加了 margin」

相關的內容至少還包括:

  • context-boxborder-box

  • padding/margin 百分比的計算方式

  • backgroundpadding/margin/border

  • margin 負值

  • margin 合併

盒模型是視覺格式化模型中的基礎單元,是 CSS 佈局模型中必不可少的一部分

CSS 盒模型描述了一個為文件樹中的元素生成的並根據視覺格式化模型進行佈局的矩形框

(引自 8 盒模型

所以,盒模型也是 CSS 在文件樹之上建立的第一層抽象,是 CSS 佈局控制與文件元素直接關聯的部分。而外邊距合併是直接影響垂直格式化的因素之一,有必要深入理解

一.經典場景

下列例子中,假設 UA 沒有預設樣式表,未宣告的樣式屬性都依照規範取其初始值

另外,假設 UA 都是遵守 CSS 規範的

1. 列表項目間的外邊距合併

li {
    margin: 8px;
}

那麼列表項目之間的間距是多少?

.li-case1 li {
    margin: 8px;
    /* 添個上內邊距 */
    padding-top: 1px;
}

.li-case2 li {
    margin: 8px;
    /* 添個下邊框 */
    border-bottom: 1px solid;
}

在 case1 和 case2 中,列表項目間距分別是多少?

2. 深層巢狀的外邊距合併

/* 縮排表示對應文件結構的巢狀關係 */
div.outer,
  div.container,
    div.content,
      div.inner {
    margin: 10px;
    min-width: 100px;
    min-height: 100px;
}

這 4 個巢狀的 div 渲染結果是什麼樣子?

div.outer,
  div.container,
    div.content,
      div.inner {
    margin: 10px;
    min-width: 100px;
    min-height: 100px;
    /* 添個border */
    border: 1px solid;
}

現在呢?

div.outer,
  div.container,
    div.content,
      div.inner {
    margin: 10px;
    /* 刪掉min-width, min-height和border */
}

那麼現在呢?

3. 帶間隙的外邊距合併

div.container {
    border-top: 1px solid;
    background: #ccc;
    margin-bottom: 60px;
}
  /* 縮排表示對應文件結構的巢狀關係 */
  div.float {
      float: left;
      width: 100px;
      height: 50px;
  }
  div.following-float {
      clear: left;
      margin-top: 50px;
  }
div.following-container {
    color: red;
}

紅色文本頂端距 .following-float 底端的距離是多少?

div.container {
    border-top: 1px solid;
    background: #ccc;
    margin-bottom: 60px;
}
  /* 縮排表示對應文件結構的巢狀關係 */
  div.float {
      float: left;
      width: 100px;
      height: 50px;
  }
  div.following-float {
      clear: left;
      /* 把50改成49 */
      margin-top: 49px;
  }
div.following-container {
    color: red;
}

現在呢?

再把 50 改成 051 呢?又分別會出現什麼情況?

P.S. 這些問題的答案此刻還是未知的,因為 Demo 還沒開始寫 ;-) 那麼就有了足夠的時間容我們認真猜一下

二.合併條件

什麼樣的外邊距會發生合併?

水平外邊距不合併。相鄰的垂直外邊距會合併,除了 2 種特殊情況:

  • 根元素盒的外邊距不合併

  • 如果一個帶有間隙的元素的上外邊距與下外邊距相鄰,它的外邊距會和緊挨著的兄弟(元素)的相鄰外邊距合併,但合併後不會再和父級塊的下外邊距合併

第 1 條跳過,對根元素套用外邊距不在情理之中

第 2 條引入了一個新概念,叫「間隙」,英文名 clearance,看樣子與 clear 屬性有關,實際符合直覺,是指 clear 屬性導致元素位置移動形成的間隙,見 CSS 規範 9 視覺格式化模型 。隱含兩個關鍵點:

  • 具有 clear 屬性

  • 並且( clear 屬性)讓元素位置發生移動了

如果滿足這兩個條件,就說一個元素帶有間隙

注意:如果套用了 clear 屬性,元素的實際位置不變,比如透過 margin-top 把元素放到那個位置的,此時元素自身的佈局位置與 clear 效果位置一樣(即 clear 屬性沒有帶來額外的空間佔用,所謂的間隙),就不具有間隙。反過來,如果套用 clear 屬性,導致元素的實際位置發生了變化,即元素上方有一部分空間是 clear 屬性帶來的,那麼就算帶有間隙

帶有間隙還不夠,還要該元素的上下外邊距相鄰(意味著元素的實際高度為 0,且沒有 padding, border ),同時滿足的話,這個元素的外邊距合併會受到限制:其外邊距只和緊挨著的兄弟的相鄰外邊距合併,合併後的結果不會再和父級塊的下外邊距發生合併

P.S. 到這裡有挑戰經典場景 3 的入場券了,但還差得很遠

「相鄰」的定義

兩個外邊距在什麼情況才算「相鄰」?

  • 都屬於流內(in-flow)塊級盒,處於同一個塊格式化上下文

  • 沒有行框(line box),空隙,內邊距和邊框把它們隔開

  • 都屬於垂直相鄰框邊界(vertically-adjacent box edges)

3 句話 4 個新概念,深度優先過一下

流內

流內/流外(in-flow/out-of-flow)是指是否用常規流定位方案來佈局該元素

繼續深度優先,定位方案分 3 種:

  • 常規流。包括塊格式化、行內格式化和相對定位

  • 浮動。從常規流的位置取出來向左/右移

  • 絕對定位。從常規流中脫離出去,根據其包含塊確定自身位置

元素既沒有浮動( float 屬性的套用值為 none ),也沒有絕對定位( position 屬性的套用值不為 absolute ),並且不是根元素,那就按常規流來佈局,就屬於流內元素,否則就是流外元素

塊格式化上下文

浮動,絕對定位的元素,非塊盒的塊容器(例如 inline-blocks,table-cells 和 table-captions),以及 'overflow' 不為 'visible' 的塊盒(當該值已被傳播到視埠時除外)會為其內容建立新的塊格式化上下文

在一個塊格式化上下文中,盒在豎直方向一個接一個地放置,從包含塊的頂部開始。兩個兄弟盒之間的垂直距離由 'margin' 屬性決定

也就是說,如果沒人建立新的 BFC,那麼就處於目前 BFC。像 JS 作用域一樣,預設大家都位於最外層作用域(最外層塊格式化上下文),遇到普通塊級盒就放進塊格式化上下文,遇到特殊的(浮動,絕對定位的等等)就新建一層作用域(建立新的塊格式化上下文),它裡面的元素都放進這個內層作用域(新的塊格式化上下文)

佈局完成後從格式化上下文的角度來看,就是一系列巢狀的 BFC,每個 BFC 負責管理一組塊盒(或者說塊級元素)的佈局

注意:這裡不提行內格式化上下文,因為區分出不同的行內格式化上下文沒有太大意義(規範定義中,沒有關於跨行內格式化上下文的特殊場景)。那麼,什麼時候會建立新的行內格式化上下文?,根據規範,只在塊容器只含有行內級盒時才建立一個新的行內格式化上下文,不像 BFC 可以顯式地強制建立

P.S. 關於何時會建立新行內格式化上下文的更多討論,請查看 When does a box establish an inline formatting context?

行框

包含來自同一行的盒的矩形區域叫做行框

一個行框總是足夠高,能夠容納它包含的所有盒。

行框是 CSS 對行的抽象表示,每行元素都處於同一個行框裡。如果太長放不下出現自動換行,那麼就會為下一行再建立一個行框。另一方面,行框不是純粹的抽象定義,它具有寬度和高度,用於決定行佈局

相鄰外邊距之間「沒有行框」可以簡單理解為沒有行內元素把它們隔開

垂直相鄰框邊界

下列 4 種場景滿足外邊距都屬於垂直相鄰框邊界的情況:

  • 盒的上外邊距與其第一個流內(in-flow)孩子的上外邊距

  • 盒的下外邊距與其下一個流內緊挨著的兄弟的上外邊距

  • 最後一個流內孩子的下外邊距與其 height 計算值為 'auto' 的父元素的下外邊距

  • 盒的上外邊距和下外邊距,要求該盒沒有建立新的塊格式化上下文,並且 'min-height' 計算值為 0, 'height' 計算值為 0 或 'auto' ,還沒有流內孩子

看起來太長,我們簡化條件,假設都是流內元素的話,那麼:

  • 父子:父元素上外邊距與長子上外邊距

  • 兄弟:元素的下外邊距與右兄弟的上外邊距

  • 父子:么兒的下外邊距與父元素的下外邊距

  • 自身:0 高「真空」元素的上外邊距與下外邊距

P.S. 這裡的「真空」是指——把洋芋片抽成真空。要麼裡面什麼都沒有,要麼流內孩子都被抽離了

也就是說,「相鄰外邊距」的位置定義具體分 3 種情況:父子,兄弟和自身(自身上下外邊距合併是比較奇特的)

重新理解「相鄰」與外邊距合併

有了前面的概念鋪墊,現在我們把零散的點整合起來,先重新定義「相鄰」:

父子,兄弟或元素自身的外邊距緊挨在一起就是「相鄰」

還有一個關鍵點:緊挨。就是說這兩個外邊距沒被「牆」隔開,「牆」分 3 種:

  • 種族:雙方必須都是流內塊級盒

  • 信仰:處於同一個塊格式化上下文

  • 地域:二者之間沒有行框(line box)、空隙、內邊距和邊框

到這裡,「相鄰」已經很清楚了,我們再反推外邊距合併的定義:

非根元素的相鄰垂直外邊距會合併,帶有間隙的話,合併受限

受限是指帶有間隙元素自身上下邊距相鄰的話,只能與兄弟元素的外邊距合併,無法和父元素的下外邊距合併

三.合併條件推論

根據外邊距合併的發生條件,有 8 條推論:

  • 浮動的盒與任何其他盒之間的外邊距不會合併(甚至一個浮動盒與它的流內子級之間也不會)

  • 建立了新的塊格式化上下文的元素(例如,浮動盒與 'overflow' 不為 'visible' 的元素)的外邊距不會與它們的流內孩子合併

  • 絕對定位的盒的外邊距不會合併(甚至與它們的流內孩子也不會)

  • 內聯塊盒的外邊距不會合併(甚至與它們的流內孩子也不會)

  • 流內塊級元素的下外邊距總會與它的下一個流內塊級兄弟的上外邊距合併,除非該兄弟(元素)具有間隙

  • 流內塊級元素的上外邊距會與它的第一個流內塊級孩子的上外邊距合併,條件是該元素沒有上邊框和上內邊距,並且其孩子不具有間隙

  • 一個 'height' 為 'auto' 並且 'min-height' 為 0 的流內塊級盒的下外邊距會與它的最後一個流內塊級孩子的下外邊距合併,條件是該盒沒有下內邊距和下邊框,並且其孩子的下外邊距沒有與具有間隙的上外边距合併

  • 盒自身的外邊距也會合併,條件是 'min-height' 屬性為 0,既沒有上下邊框,也沒有上下內邊距, 'height' 為 0 或 'auto' ,且不含行框的話,那麼其所有流內孩子的外邊距(如果存在的話)都會合併

簡化總結,不過 4 條:

  • 非流內(絕對定位或浮動)不合併

  • 觸發新 BFC 建立(浮動,絕對定位元素,非塊盒的塊容器以及 'overflow' 不為 'visible' 的某些塊盒)不與孩子合併

  • 非塊級盒(內聯塊)不合併

  • 一般情況下,兄弟元素的下上外邊距,父子元素的上外邊距、下外邊距,元素自身的上下外邊距會合併

前 3 點針對「相鄰」的前提條件(流內,同 BFC,塊級盒),第 4 點是對 4 種「相鄰」場景的轉述,展開就是 8 條推論

四.合併行為

兩個相鄰外邊距發生合併後,形成的外邊距叫折疊外邊距

P.S. collapsed margin 故意譯作折疊表示結果,與合併的動作區分開

外邊距合併有 2 個特點:

  • 遞迴:即深層合併。合併一次後,再檢查與合併結果相鄰的外邊距有沒有能合併的,有的話接著合

  • 貪婪:追求最寬合併結果。兩個 margin 正值取最大值,兩個負值取絕對值的最大值

對於遞迴特性,「相鄰」的定義擴展出一條遞迴公式:

折疊外邊距也能與另一個外邊距相鄰,只要其外邊距的任意一部分與那個外邊距相鄰就算

貪婪與外邊距合併結果計算方式有關,因為 margin 允許負值,情況稍微複雜一點:

  • 都是正值,直接求二者最大值

  • 一正一負,相加求和

  • 都是負值,求二者絕對值的最大值

例如:

ul {margin-bottom: -15px;}
  /* 縮排表示對應文件結構的巢狀關係 */
  li {margin-bottom: 20px;}
h1 {margin-top: -18px;}

那麼 h1 與最後一個 li 的垂直距離為 20 + -max(|-15|, |-18|) = 2px

無論對正值還是負值,求最大值的原則都是讓合併結果儘量寬(絕對值更大的負值能讓元素內容偏移出去更遠的距離),即貪婪性

五.線上 Demo

Demo 位址: http://ayqy.net/temp/margin-collapse.html

P.S. 答案都在 Demo 裡,解釋都在源碼裡

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論