你不知道的 CSS 之包含塊

来源:https://www.cnblogs.com/xw-01/p/18259655
-Advertisement-
Play Games

你不知道的 CSS 之包含塊 一說到 CSS 盒模型,這是很多小伙伴耳熟能詳的知識,甚至有的小伙伴還能說出 border-box 和 content-box 這兩種盒模型的區別。 但是一說到 CSS 包含塊,有的小伙伴就懵圈了,什麼是包含塊?好像從來沒有聽說過這玩意兒。 好吧,如果你對包含塊的知識一 ...


你不知道的 CSS 之包含塊

一說到 CSS 盒模型,這是很多小伙伴耳熟能詳的知識,甚至有的小伙伴還能說出 border-box 和 content-box 這兩種盒模型的區別。

但是一說到 CSS 包含塊,有的小伙伴就懵圈了,什麼是包含塊?好像從來沒有聽說過這玩意兒。

image-20220814222004395

好吧,如果你對包含塊的知識一無所知,那麼系好安全帶,咱們準備出發了。

image-20220813140434032

包含塊英語全稱為containing block,實際上平時你在書寫 CSS 時,大多數情況下你是感受不到它的存在,因此你不知道這個知識點也是一件很正常的事情。但是這玩意兒是確確實實存在的,在 CSS 規範中也是明確書寫了的:

https://drafts.csswg.org/css2/#containing-block-details

image-20220814222458695

並且,如果你不瞭解它的運作機制,有時就會出現一些你認為的莫名其妙的現象。

那麼,這個包含塊究竟說了什麼內容呢?

說起來也簡單,就是元素的尺寸和位置,會受它的包含塊所影響。對於一些屬性,例如 width, height, padding, margin,絕對定位元素的偏移值(比如 position 被設置為 absolute 或 fixed),當我們對其賦予百分比值時,這些值的計算值,就是通過元素的包含塊計算得來。

來吧,少年,讓我們從最簡單的 case 開始看。

image-20220814223152726
<body>
  <div class="container">
    <div class="item"></div>
  </div>
</body>
.container{
  width: 500px;
  height: 300px;
  background-color: skyblue;
}
.item{
  width: 50%;
  height: 50%;
  background-color: red;
}

請仔細閱讀上面的代碼,然後你認為 div.item 這個盒子的寬高是多少?

image-20220814223451349

相信你能夠很自信的回答這個簡單的問題,div.item 盒子的 width 為 250px,height 為 150px。

這個答案確實是沒有問題的,但是如果我追問你是怎麼得到這個答案的,我猜不瞭解包含塊的你大概率會說,因為它的父元素 div.container 的 width 為 500px,50% 就是 250px,height 為 300px,因此 50% 就是 150px。

這個答案實際上是不准確的。正確的答案應該是,div.item 的寬高是根據它的包含塊來計算的,而這裡包含塊的大小,正是這個元素最近的祖先塊元素的內容區。

因此正如我前面所說,很多時候你都感受不到包含塊的存在。

包含塊分為兩種,一種是根元素(HTML 元素)所在的包含塊,被稱之為初始包含塊(initial containing block)。對於瀏覽器而言,初始包含塊的的大小等於視口 viewport 的大小,基點在畫布的原點(視口左上角)。它是作為元素絕對定位和固定定位的參照物。

另外一種是對於非根元素,對於非根元素的包含塊判定就有幾種不同的情況了。大致可以分為如下幾種:

  • 如果元素的 positiion 是 relative 或 static ,那麼包含塊由離它最近的塊容器(block container)的內容區域(content area)的邊緣建立。
  • 如果 position 屬性是 fixed,那麼包含塊由視口建立。
  • 如果元素使用了 absolute 定位,則包含塊由它的最近的 position 的值不是 static (也就是值為fixed、absolute、relative 或 sticky)的祖先元素的內邊距區的邊緣組成。

前面兩條實際上都還比較好理解,第三條往往是初學者容易比較忽視的,我們來看一個示例:

<body>
    <div class="container">
      <div class="item">
        <div class="item2"></div>
      </div>
    </div>
  </body>
.container {
  width: 500px;
  height: 300px;
  background-color: skyblue;
  position: relative;
}
.item {
  width: 300px;
  height: 150px;
  border: 5px solid;
  margin-left: 100px;
}
.item2 {
  width: 100px;
  height: 100px;
  background-color: red;
  position: absolute;
  left: 10px;
  top: 10px;
}

首先閱讀上面的代碼,然後你能在腦海裡面想出其大致的樣子麽?或者用筆和紙畫一下也行。

公佈正確答案:

image-20220814233548188

怎麼樣?有沒有和你所想象的對上?

其實原因也非常簡單,根據上面的第三條規則,對於 div.item2 來講,它的包含塊應該是 div.container,而非 div.item。

如果你能把上面非根元素的包含塊判定規則掌握,那麼關於包含塊的知識你就已經掌握 80% 了。

實際上對於非根元素來講,包含塊還有一種可能,那就是如果 position 屬性是 absolute 或 fixed,包含塊也可能是由滿足以下條件的最近父級元素的內邊距區的邊緣組成的:

  • transform 或 perspective 的值不是 none
  • will-change 的值是 transform 或 perspective
  • filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效).
  • contain 的值是 paint (例如: contain: paint;)

我們還是來看一個示例:

<body>
  <div class="container">
    <div class="item">
      <div class="item2"></div>
    </div>
  </div>
</body>
.container {
  width: 500px;
  height: 300px;
  background-color: skyblue;
  position: relative;
}
.item {
  width: 300px;
  height: 150px;
  border: 5px solid;
  margin-left: 100px;
  transform: rotate(0deg); /* 新增代碼 */
}
.item2 {
  width: 100px;
  height: 100px;
  background-color: red;
  position: absolute;
  left: 10px;
  top: 10px;
}

我們對於上面的代碼只新增了一條聲明,那就是 transform: rotate(0deg),此時的渲染效果卻發生了改變,如下圖所示:

image-20220814234347149

可以看到,此時對於 div.item2 來講,包含塊就變成了 div.item。

好了,到這裡,關於包含塊的知識就基本講完了。

image-20220814234654914

我們再把 CSS 規範中所舉的例子來看一下。

<html>
  <head>
    <title>Illustration of containing blocks</title>
  </head>
  <body id="body">
    <div id="div1">
      <p id="p1">This is text in the first paragraph...</p>
      <p id="p2">
        This is text
        <em id="em1">
          in the
          <strong id="strong1">second</strong>
          paragraph.
        </em>
      </p>
    </div>
  </body>
</html>

上面是一段簡單的 HTML 代碼,在沒有添加任何 CSS 代碼的情況下,你能說出各自的包含塊麽?

對應的結果如下:

元素 包含塊
html initial C.B. (UA-dependent)
body html
div1 body
p1 div1
p2 div1
em1 p2
strong1 p2

首先 HTML 作為根元素,對應的包含塊就是前面我們所說的初始包含塊,而對於 body 而言,這是一個 static 定位的元素,因此該元素的包含塊參照第一條為 html,以此類推 div1、p1、p2 以及 em1 的包含塊也都是它們的父元素。

不過 strong1 比較例外,它的包含塊確實 p2,而非 em1。為什麼會這樣?建議你再把非根元素的第一條規則讀一下:

  • 如果元素的 positiion 是 relative 或 static ,那麼包含塊由離它最近的塊容器(block container)的內容區域(content area)的邊緣建立。

沒錯,因為 em1 不是塊容器,而包含塊是離它最近的塊容器的內容區域,所以是 p2。

接下來添加如下的 CSS:

#div1 { 
  position: absolute; 
  left: 50px; top: 50px 
}

上面的代碼我們對 div1 進行了定位,那麼此時的包含塊會發生變化麽?你可以先在自己思考一下。

答案如下:

元素 包含塊
html initial C.B. (UA-dependent)
body html
div1 initial C.B. (UA-dependent)
p1 div1
p2 div1
em1 p2
strong1 p2

可以看到,這裡 div1 的包含塊就發生了變化,變為了初始包含塊。這裡你可以參考前文中的這兩句話:

  • 初始包含塊(initial containing block)。對於瀏覽器而言,初始包含塊的的大小等於視口 viewport 的大小,基點在畫布的原點(視口左上角)。它是作為元素絕對定位和固定定位的參照物。
  • 如果元素使用了 absolute 定位,則包含塊由它的最近的 position 的值不是 static (也就是值為fixed、absolute、relative 或 sticky)的祖先元素的內邊距區的邊緣組成。

是不是一下子就理解了。沒錯,因為我們對 div1 進行了定位,因此它會應用非根元素包含塊計算規則的第三條規則,尋找離它最近的 position 的值不是 static 的祖先元素,不過顯然 body 的定位方式為 static,因此 div1 的包含塊最終就變成了初始包含塊。

接下來我們繼續修改我們的 CSS:

#div1 { 
  position: absolute; 
  left: 50px; 
  top: 50px 
}
#em1  { 
  position: absolute; 
  left: 100px; 
  top: 100px 
}

這裡我們對 em1 同樣進行了 absolute 絕對定位,你想一想會有什麼樣的變化?

沒錯,聰明的你大概應該知道,em1 的包含塊不再是 p2,而變成了 div1,而 strong1 的包含塊也不再是 p2 了,而是變成了 em1。

如下表所示:

元素 包含塊
html initial C.B. (UA-dependent)
body html
div1 initial C.B. (UA-dependent)
p1 div1
p2 div1
em1 div1(因為定位了,參閱非根元素包含塊確定規則的第三條)
strong1 em1(因為 em1 變為了塊容器,參閱非根元素包含塊確定規則的第一條)

好了,這就是 CSS 規範中所舉的例子。如果你全都能看明白,以後你還能跟別人說你是看過這一塊知識對應的 CSS 規範的人。

image-20220815093518833

另外,關於包含塊的知識,在 MDN 上除瞭解說了什麼是包含塊以外,也舉出了很多簡單易懂的示例。

具體你可以移步到:https://developer.mozilla.org/zh-CN/docs/Web/CSS/Containing_block

好了,這就是有關包含塊的所有內容了,你學會了麽?-)


附加

面試題

瀏覽器是如何渲染頁面的?

當瀏覽器的網路線程收到 HTML 文檔後,會產生一個渲染任務,並將其傳遞給渲染主線程的消息隊列。

在事件迴圈機制的作用下,渲染主線程取出消息隊列中的渲染任務,開啟渲染流程。


整個渲染流程分為多個階段,分別是: HTML 解析、樣式計算、佈局、分層、繪製、分塊、光柵化、畫

每個階段都有明確的輸入輸出,上一個階段的輸出會成為下一個階段的輸入。

這樣,整個渲染流程就形成了一套組織嚴密的生產流水線。


渲染的第一步是解析 HTML

解析過程中遇到 CSS 解析 CSS,遇到 JS 執行 JS。為了提高解析效率,瀏覽器在開始解析前,會啟動一個預解析的線程,率先下載 HTML 中的外部 CSS 文件和 外部的 JS 文件。

如果主線程解析到link位置,此時外部的 CSS 文件還沒有下載解析好,主線程不會等待,繼續解析後續的 HTML。這是因為下載和解析 CSS 的工作是在預解析線程中進行的。這就是 CSS 不會阻塞 HTML 解析的根本原因。

如果主線程解析到script位置,會停止解析 HTML,轉而等待 JS 文件下載好,並將全局代碼解析執行完成後,才能繼續解析 HTML。這是因為 JS 代碼的執行過程可能會修改當前的 DOM 樹,所以 DOM 樹的生成必須暫停。這就是 JS 會阻塞 HTML 解析的根本原因。

第一步完成後,會得到 DOM 樹和 CSSOM 樹,瀏覽器的預設樣式、內部樣式、外部樣式、行內樣式均會包含在 CSSOM 樹中。


渲染的下一步是樣式計算

主線程會遍歷得到的 DOM 樹,依次為樹中的每個節點計算出它最終的樣式,稱之為 Computed Style。

在這一過程中,很多預設值會變成絕對值,比如red會變成rgb(255,0,0);相對單位會變成絕對單位,比如em會變成px

這一步完成後,會得到一棵帶有樣式的 DOM 樹。


接下來是佈局,佈局完成後會得到佈局樹。

佈局階段會依次遍歷 DOM 樹的每一個節點,計算每個節點的幾何信息。例如節點的寬高、相對包含塊的位置。

大部分時候,DOM 樹和佈局樹並非一一對應。

比如display:none的節點沒有幾何信息,因此不會生成到佈局樹;又比如使用了偽元素選擇器,雖然 DOM 樹中不存在這些偽元素節點,但它們擁有幾何信息,所以會生成到佈局樹中。還有匿名行盒、匿名塊盒等等都會導致 DOM 樹和佈局樹無法一一對應。


下一步是分層

主線程會使用一套複雜的策略對整個佈局樹中進行分層。

分層的好處在於,將來某一個層改變後,僅會對該層進行後續處理,從而提升效率。

滾動條、堆疊上下文、transform、opacity 等樣式都會或多或少的影響分層結果,也可以通過will-change屬性更大程度的影響分層結果。


再下一步是繪製

主線程會為每個層單獨產生繪製指令集,用於描述這一層的內容該如何畫出來。


完成繪製後,主線程將每個圖層的繪製信息提交給合成線程,剩餘工作將由合成線程完成。

合成線程首先對每個圖層進行分塊,將其劃分為更多的小區域。

它會從線程池中拿取多個線程來完成分塊工作。


分塊完成後,進入光柵化階段。

合成線程會將塊信息交給 GPU 進程,以極高的速度完成光柵化。

GPU 進程會開啟多個線程來完成光柵化,並且優先處理靠近視口區域的塊。

光柵化的結果,就是一塊一塊的點陣圖


最後一個階段就是

合成線程拿到每個層、每個塊的點陣圖後,生成一個個「指引(quad)」信息。

指引會標識出每個點陣圖應該畫到屏幕的哪個位置,以及會考慮到旋轉、縮放等變形。

變形發生在合成線程,與渲染主線程無關,這就是transform效率高的本質原因。

合成線程會把 quad 提交給 GPU 進程,由 GPU 進程產生系統調用,提交給 GPU 硬體,完成最終的屏幕成像。

什麼是 reflow?

reflow 的本質就是重新計算 layout 樹。

當進行了會影響佈局樹的操作後,需要重新計算佈局樹,會引發 layout。

為了避免連續的多次操作導致佈局樹反覆計算,瀏覽器會合併這些操作,當 JS 代碼全部完成後再進行統一計算。所以,改動屬性造成的 reflow 是非同步完成的。

也同樣因為如此,當 JS 獲取佈局屬性時,就可能造成無法獲取到最新的佈局信息。

瀏覽器在反覆權衡下,最終決定獲取屬性立即 reflow。

什麼是 repaint?

repaint 的本質就是重新根據分層信息計算了繪製指令。

當改動了可見樣式後,就需要重新計算,會引發 repaint。

由於元素的佈局信息也屬於可見樣式,所以 reflow 一定會引起 repaint。

為什麼 transform 的效率高?

因為 transform 既不會影響佈局也不會影響繪製指令,它影響的只是渲染流程的最後一個「draw」階段

由於 draw 階段在合成線程中,所以 transform 的變化幾乎不會影響渲染主線程。反之,渲染主線程無論如何忙碌,也不會影響 transform 的變化。


-EOF-


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 摘要:“本文深入探討了Nuxt3 Composables,重點介紹了其目錄架構和內置API的高效應用。通過學習本文,讀者將能夠更好地理解和利用Nuxt3 Composables來構建高效的應用程式。” ...
  • 依賴分類 依賴根據開發環境需要和實際運行環境需要,分為dependencies和devDependencies。 例如:typescript和eslint屬於devDependencies,而vue和axios等屬於dependencies。 版本號組成 版本由兩部分組成,一是前面的首碼符號,二是版 ...
  • 摘要:該文指南詳述了Nuxt 3的概況與安裝,聚焦於在Nuxt 3框架下運用Vuex進行高效的狀態管理,涵蓋基礎配置、模塊化實踐至高階策略,助力開發者構建高性能前後端分離應用。 ...
  • ‍ 寫在開頭 點贊 + 收藏 學會 如何解決uniapp H5本地代理實現跨域訪問? 1.第一種解決方法: 直接創建一個vue.config.js文件,併在裡面配置devServer,直接上代碼,重啟跑項目 親測有效 // vue.config.js module.exports ...
  • 很久很久沒有更新博客了,因為實在是太忙了,每天都有公司的事情忙不完....... 最近在做車輛模擬地圖,在實現控制站點名稱按需顯示時,折騰了好一段時間,特此記錄一下。最終界面如下圖所示: 站點顯示需求:首末站必須顯示,從第一個站開始,如果站點名稱能顯示下,則顯示,如果站點名稱會重疊則隱藏,以此類推。 ...
  • 摘要:本文是一份關於Nuxt 3路由系統的詳盡指南。它從介紹Nuxt 3的基本概念開始,包括Nuxt 3與Nuxt 2的區別和選擇Nuxt 3的理由。然後,它詳細解釋了安裝和配置Nuxt 3的步驟,以及Nuxt 3路由系統的基礎知識,如動態路由和嵌套路由。接著,它介紹了路由中間件的作用和編寫自定義中... ...
  • 最近需要做一個三維場景切換的功能,切換場景後,還可以進行二三維模式的切換,二三維切換時,要定位到當前場景視角,那麼場景的視角參數信息就需要保存到狀態數據中,以供二三維場景切換時使用。 項目是用vue做的,這裡並沒有使用vue的狀態管理庫,我是這樣實現的: 定義狀態數據sceneInfo let sc ...
  • 你習慣在js代碼中使用async await嗎? 我經常在js代碼中寫一些非同步方法,使用await調用的地方,如果方便修改成非同步方法,就修改成非同步方法,如下所示: async setPosition(graphic, lng, lat) { this.lng = lng; this.lat = la ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...