你不知道的 flex-shrink 計算規則


對於 flex-shrink 我們都知道它在 flex 布局中控制 flex 盒子空間不足時子元素改如何收縮,平常開發中更多的是使用默認值 1 或者設置 0。
那設置其他值的時候會有什么效果呢,不少文章中描述都不是很細,在很長一段時間我甚至以為自己是了解它的。

開篇我們帶着幾個問題
1. “flex-shrink 屬性定義了項目的縮小比例,當父元素主軸方向空間不足的時候,子元素們按照 flex-shrink 的比例來縮小。” 這句描述對嗎?
2. 一個父元素下有兩個子元素,兩個子元素各占用父元素 50% 且分別有 50px、20px 的 padding。這個很簡單的需求用 flex 布局如何實現?如果嘗試以后和你的想象不同,那為什么會這樣呢?
3. 當空間不足時,各項目具體會縮小多少?子元素 `flex-shrink` 不同時有何影響?子元素寬度會對縮小有影響嗎?父子元素的 margin、padding、border 會對結果有影響嗎?box-sizing 的值會有影響嗎?

如果你對以上的問題不能清楚的回答,或者嘗試以后發現和自己想象的不同,那這篇文章對於你可能會有一些用。

首先我們看第一個問題
> 1. “flex-shrink 屬性定義了項目的縮小比例,當父元素主軸方向空間不足的時候,子元素們按照 flex-shrink 的比例來縮小。” 這句描述對嗎?
這句話描述其實不准確。
flex-shrink 決定了子元素縮小系數,但在具體計算的時候,其實它還受到了 flex base size 的影響。
w3c 對於的 flex-shrink 的描述有這樣一段備注
> Note: The flex shrink factor is multiplied by the flex base size when distributing negative space. This distributes negative space in proportion to how much the item is able to shrink, so that e.g. a small item won’t shrink to zero before a larger item has been noticeably reduced.

從中我們可以看到,真正使用的縮小系數其實是 flex shrink factor * flex base size。下面我們用一個例子來說明它

<style>
    .box {
        display: flex;
        width: 400px;
        outline: 1px red solid;

    }

    .item1 {
        flex: 0 2 300px;
        background-color: #32d6d6;
    }

    .item2 {
        flex: 0 1 200px;
        background-color: #e2a83e;
    }

    .item3 {
        flex: 0 2 100px;
        background-color: #b85ad0;
    }
</style>
...
<div class="box">
    <div class="item1">1</div>
    <div class="item2">2</div>
    <div class="item3">3</div>
</div>    

按照不准確的描述 `flex-shrink` 決定了子元素縮小系數,那我們知道子元素需要的空間是 300+200+100 一共 600px,但父元素只有 400px
所以分別的負空間是 200px,或者說需要縮小 200px。三個元素 `flex-shrink` 分別為 2 1 2,表面上看應該分別縮小 80 40 80,那三個元素應該 220 160 20。但事實是這樣嗎?
如果你也嘗試一下,就會知道,實際上的效果是 180 160 60。
我們來看一下正確的計算方式:

flex-shrink * flex-base(姑且先這么寫,之后會修正) => factor
2 * 300 => 600
1 * 200 => 200
2 * 100 => 200

所以三個元素真正的系數分別是 600/1000 200/1000 200/1000。200 的總額得出 120 40 40。可以看到和實際情況相符。

按照這個公式可以滿足多數情況的使用,但其中還隱藏着其他規則。下面我們看第二個問題
>2. 一個父元素下有兩個子元素,兩個子元素各占用父元素 50% 且分別有 50px、20px 的 padding。這個很簡單的需求用 flex 布局如何實現?如果嘗試以后和你的想象不同,那為什么會這樣呢?

該問題其實是我發現自己對 `flex-shrink` 不夠了解,從而研究的原因。
這個問題看起來很簡單吧,估計多數人第一反應是這樣:

<style>
    .box {
        display: flex;
        width: 400px;
        outline: 1px red solid;
    }
    .item-2-1 {
        flex: 1 1;
        padding: 50px;
        background-color: #32d6d6;
        background-clip: content-box;
    }
    .item-2-2 {
        flex: 1 1;
        padding: 20px;
        background-color: #b85ad0;
        background-clip: content-box;
    }
</style>
...
<div class="box">
    <div class="item-2-1">1</div>
    <div class="item-2-2">2</div>
</div>

看起來收縮、放大系數都相等,兩個元素應該父元素 400 像素,每個 200 對吧?我們看一下實際情況



是不是和想象不同?

下面說明原因
w3c 里對於元素可用長度有這樣的描述
>dimension of the flex container’s content box is a definite size, use that; if that dimension of the flex container is being sized under a min or max-content constraint, the available space in that dimension is that constraint; otherwise, subtract the flex container’s margin, border, and padding from the space available to the flex container in that dimension and use that value. This might result in an infinite value.

我們可用看到其實計算主軸可以用長度的計算是要去除 margin, border, and padding 的,但這里的描述我覺得其實也不准確,他這里說的只是 flex container,似乎只是父元素里的 margin、border、padding。但在我的實際測試的時候,其實還包括更多
比如:直接子元素的 margin、border、padding 甚至是直接子元素的 min-width,稍緩我會說為什么

那么我們來計算一下
400 - 50*2 - 20*2 = 260(可用空間)
flex-base 為 0,flex-grow 都為 1;260*(1/2)= 130
130 + 20 + 20 = 170

那么我們要如何實現子元素含 padding 時也平分空間呢?
flex-base 的描述里有這樣的一句
> As another corollary, flex-basis determines the size of the content box, unless otherwise specified such as by box-sizing [CSS3UI].

可以看到 flex-basis 的數值設置的 width 其實是 box-sizing 的默認值 box-sizing: content-box;
那么理所當然會想到修改參數,改為 box-sizing: border-box; 然后 flex-base: 100%;
如此一來,兩個元素都如 IE 盒模型一樣,寬度包含 border padding,而且收縮、放大系數都一樣,是不是就可以實現需求了呢?答案還是否定的,不但實現不了需求,甚至會出現一時間難以理解的數值

上面提到子元素的 padding 等值也會算在不可伸縮長度里凍結掉。為什么這么說呢,我們結合上圖的原因來做解說

這個值是怎么來的呢,其經過了以下的步驟
1. 計算子元素 flex-base 所代表的實際值 => 400px
2. 那兩個就是 800px,父元素 400px,主軸長度不夠,flex-shrink 開始生效。但第二步卻不是 flex-shrink * flex-base 得出真正的比例系數,我們需要先得到“真正的” flex base size,其實之前我們提及過
 事實上,真正的 flex base size 並非單純的是 flex-base。更准確的說,子元素 flex-base 設置后帶來的 content width。比如這里 flex base size = box width 400 - padding 20*2 - border 0 = 360,以及兩外一個 400-50*2=300。
3. 計算比例系數 360*1=360,300*1=300。所有其比例系數分別是 300/660、360/660。
4. 計算需要分配的負空間 400*2-400 = 400px
5. 計算分別需要縮減的部分 400*(300/660)≈181.8181、400*(360/660)≈218.1818
6. 實際寬度 400-181.8181≈218.18 400-21≈181.81

對於其原因,w3c 里對於如何計算彈性長度有這樣的一個描述 
> Size inflexible items. Freeze, setting its target main size to its hypothetical main size…

對此我是這樣理解的,在計算子元素主軸長度的時候,有這么一些操作
把 flex container 的 margin、border、padding 所占的長度凍結,因為這些不可分配
把 子元素的 margin、border、padding 所占的空間凍結,因為這部分不會參與伸縮
剩下的空間才會作為正、負長度分配給子元素

所以我們得出更詳細的壓縮計算公式

flex_container_available_length = flex_container_content_width(or height)
flex_items_length = flex_item_box_width + flex_item_box_width + flex_item_box_width...
shrink_factor = (flex-shrink * flex_base_size) /((flex-shrink * flex_base_size) + (flex-shrink * flex_base_size) + ...)
will_allocate_length = flex_container - flex_items_length
flex_item_width = flex_item_box_width + flex_item_box_width * (will_allocate_length * shrink_factor)

注1:flex_item_box_width = margin + padding + border + content width
注2: flex_base_size 取決於該元素的 box-sizing 和 flex-base,其值為 border-box 時,flex_base_size = flex-base - padding - barder;其值為 content-box 時,flex_base_size = flex-base。

為了驗證公式的正確性,我們隨意設計一個 margin padding border box-sizing flex-shrink flex-base 多樣繁雜的 demo(.flexBox)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>flex-shrink demo</title>
<style>
    .box {
        display: flex;
        width: 400px;
        outline: 1px red solid;
    }

    .item1 {
        flex: 0 2 300px;
        background-color: #32d6d6;
    }

    .item2 {
        flex: 0 1 200px;
        background-color: #e2a83e;
    }

    .item3 {
        flex: 0 2 100px;
        background-color: #b85ad0;
    }

    .item-2-1 {
        flex: 1 1;
        padding: 50px;
        background-color: #32d6d6;
        background-clip: content-box;
    }
    .item-2-2 {
        flex: 1 1;
        padding: 20px;
        background-color: #b85ad0;
        background-clip: content-box;
    }

    .demo-3 {
        flex: 1 1;
        display: flex;
        align-items: center;
    }
    .demo-3-1 {
        flex: 1 1;
        padding: 50px;
        background-color: #32d6d6;
        background-clip: content-box;
    }
    .demo-3-2 {
        flex: 1 1;
        padding: 20px;
        background-color: #b85ad0;
        background-clip: content-box;
    }

    .item-border-box {
        flex-basis: 100%;
        box-sizing: border-box;
    }

    .flexBox {
        display: flex;
        width: 1500px;
        outline: 1px red solid;
    }
    .flexItem-1, .flexItem-2, .flexItem-3 {
        flex-shrink: 2;
        flex-basis: 300px;
    }
    .flexItem-1 {
        margin: 0 10px;
    }
    .flexItem-2 {
        padding: 0 20px;
        border: 5px #ccc solid;
        box-sizing: content-box;
    }
    .flexItem-3 {
        padding: 0 20px;
        border: 5px #ccc solid;
        box-sizing: border-box;
    }
    .flexItem-4, .flexItem-5, .flexItem-6 {
        flex-shrink: 1;
        flex-basis: 200px;
    }
    .flexItem-4 {
        padding: 0 10px;
    }
    .flexItem-5 {
        border: 5px #ccc solid;
        margin: 0 10px;
        box-sizing: content-box;
    }
    .flexItem-6 {
        border: 5px #ccc solid;
        margin: 0 10px;
        box-sizing: border-box;
    }
    .flexItem-7, .flexItem-8, .flexItem-9 {
        flex-shrink: 2;
        flex-basis: 100px;
    }
    .flexItem-7 {
        border: 5px #ccc solid;
    }
    .flexItem-8 {
        padding: 0 30px;
        margin: 0 10px;
        box-sizing: content-box;
    }
    .flexItem-9 {
        padding: 0 30px;
        margin: 0 10px;
        box-sizing: border-box;
    }
</style>
</head>

<body>
    <div class="box">
        <div class="item1">1</div>
        <div class="item2">2</div>
        <div class="item3">3</div>
    </div>
    <br />

    <div class="box">
        <div class="item-2-1">1</div>
        <div class="item-2-2">2</div>
    </div>
    <br />

    <div class="box">
        <div class="demo-3">
            <div class="demo-3-1">
                1
            </div>
        </div>
        <div class="demo-3">
            <div class="demo-3-2">
                2
            </div>
        </div>
    </div>
    <br />

    <div class="box">
        <div class="item-2-1 item-border-box">1</div>
        <div class="item-2-2 item-border-box">2</div>
    </div>
    <br />

    <div class="flexBox">
        <div class="flexItem-1" title="300 - (550*600/2770) ≈ 180.866">1</div>
        <div class="flexItem-2" title="300 - (550*600/2770) ≈ 180.866">2</div>
        <div class="flexItem-3" title="300 - (550*500/2770) ≈ 200.722 - 20*2 - 10*2 = 150.722">3</div>
        <div class="flexItem-4" title="200 - (550*200/2770) ≈ 160.288">4</div>
        <div class="flexItem-5" title="200 - (550*200/2770) ≈ 160.288">5</div>
        <div class="flexItem-6" title="200 - (550*190/2770) ≈ 162.274 - 5*2 = 152.274">6</div>
        <div class="flexItem-7" title="100 - (550*200/2770) ≈ 60.288">7</div>
        <div class="flexItem-8" title="100 - (550*200/2770) ≈ 60.288">8</div>
        <div class="flexItem-9" title="100 - (550*80/2770) ≈ 84.115 - 30*2 = 24.115">9</div>
    </div>
    <!--
    flex_container_available_length = 1500
    flex_items_length = (300+10*2) + (300+20*2+5*2) + (300) + (200+10*2) + (200+10*2+5*2) + (200+10*2) + (100+5*2) + (100+10*2+30*2) + (100+10*2)
                      = 2050
    shrink_factor = 2770
        300*2  600
        300*2  600
        (300-20*2-5*2)*2  500
    
        200*1 200
        200*1 200
        (200-5*2)*1 190
    
        100*2  200
        100*2  200
        (100-30*2)*2  80
    
    will_allocate_length = 1500 - 2050 = -550
    
    flex_item_width
        300 - (550*600/2770) = 180.866
        300 - (550*600/2770) = 180.866
        300 - (550*500/2770) = 200.722 - 20*2 - 10*2 = 150.722
    
        200 - (550*200/2770) = 160.288
        200 - (550*200/2770) = 160.288
        200 - (550*190/2770) = 162.274 - 5*2 = 152.274
        
        100 - (550*200/2770) = 60.288
        100 - (550*200/2770) = 60.288
        100 - (550*80/2770) = 84.115 - 30*2 = 24.115
    -->
</body>

</html>

 

上面的代碼包括文章中所有的 demo 的代碼,和第二個問題里說的需求的解決方法(其實巨簡單) 

文章里留下的另一個坑,min-width 會對計算有什么影響呢?這個問題留給你自己嘗試思考吧。如果想不通,也歡迎留言討論。

最后,附上資料, 同時感謝stackoverflow的幫助。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM