CSS中一些属性的计算规则

昨天遇见了一个关于flex-shrink计算规则的问题,一时语塞,发现自己居然没有认真研究过这里面的东西,因此在合理整理了关于CSS中一些属性的计算规则。

<!--more-->

本文主要参考《CSS权威指南》和MDN上相关文档,整理一些属性的计算规则,因此章节顺序不影响阅读,但可能需要提前了解如BFC、包含块之类的概念。

1. padding百分比

参考:包含块

一个元素的包含块由其position属性确定。

  • 如果 position 属性为 static 或 relative ,包含块就是由它的最近的祖先块元素
  • 如果 position 属性为 absolute ,包含块就是由它的最近的 position 的值不是 static 的祖先元素的内边距区的边缘组成。
  • 如果 position 属性是 fixed,包含块就是当前窗口

如果某些属性被赋予一个百分值的话,它的计算值是由这个元素的包含块计算而来的。这些属性包括盒模型属性和偏移属性:

  • 要计算 height top 及 bottom ,这些属性由包含块的 height 属性的值来计算它的百分值。如果包含块的 height 值会根据它的内容变化,而且包含块的 position 属性的值被赋予 relative 或 static ,那么,这些值的计算值为 0。
  • 要计算 width, left, right, padding, margin 这些属性由包含块的 width 属性的值来计算它的百分值。

padding百分比的计算规则:当设置padding时,如果使用百分数,其取值是相对于其包含块的宽度进行计算的。

基于此规则,我们可以实现一个宽高成比例的容器(如正方形)。下面是一个实现正方形容器的示例

<style>
    /* 当容器的width非固定值时就很有效 */
    .box {background-color: red;position: relative;width: 100px;}
    /* 由after撑开父元素 */
    .box::after {
        content: '';display: block;width: 100%;
        padding-top: 100%; /* 此处就可以实现宽高按比例设置 */
    }
    /* 具体的内容在box_inner编写 */
    .box_inner {position: absolute;z-index: 1;left: 0;top: 0;width: 100%;height: 100%;}
</style>
<div class="box">
    <div class="box_inner">content in here</div>
</div>

2. 定位时过分受限

定位相关属性top,right,bottom,left描述了距离包含块对应边的偏移。

当同时声明了left和right会自动计算width的大小;当同时声明了top和bottom会自动计算height的大小;

如果同时设置了top,right,bottom,left,width,height,然后指定了margin:auto就会导致元素出现过分受限,此时浏览器都会忽略偏移量指定值,并重新计算,因此可以利用这个规则实现垂直居中

.box {position: absolute;
    width: 100px;height: 100px;
    left: 0;right: 0;top: 0;bottom: 0;
    margin: auto 0;
}

3. line-height

line-height计算规则如下:

  • 如果使用 em,ex 和百分数直接指定当前元素的行高,都是相对于当前元素的 font-size 进行计算
  • 如果是从父元素继承行高:
    • 如果父元素指定的是固定数值,则直接使用该值
    • 如果父元素使用百分数来设置行高,浏览器会首先计算其父元素的字体大小与对应百分数的乘积,得到对应的结果再传递给对应元素作为行高,* 如果父元素使用em,与使用百分数的情形相同
    • 如果父元素使用无单位的乘积因子来指定行高,浏览器会计算该元素的字体大小,然后乘以对应的乘积因子,并将结果应用在行高之上。

基于以上原因,MDN上的示例代码推荐使用无单位的乘积因子来设置需要继承的行高。

下面是一个例子:求下面s1、s2、s5、s6的font-size和line-height分别是多少px

.p1 {font-size: 16px; line-height: 32px;}
.s1 {font-size: 2em;} // 直接继承父元素的行高32px
.s2 {font-size: 2em; line-height: 2em;} // 根据当前元素的font-size设置行高 (16*2)*2

.p2 {font-size: 16px; line-height: 2;}
.s5 {font-size: 2em;}  // 直接继承父元素的行高为乘积因子2,然后使用当前元素的font-size参与计算 2em * 2
.s6 {font-size: 2em; line-height: 2em;} //同上,根据当前元素的font-size计算行高

.p3 {font-size: 16px; line-height: 2em}
.s5 {font-size: 2em;} // 父元素行高设置的是em,先在父元素进行计算2*16,然后再将该结果应用在子元素上
.s6 {font-size: 2em; line-height: 2em} //同上,根据当前元素的font-size计算行高
<div class="p1">
    <div class="s1">1</div> <!--32px 32px-->
    <div class="s2">1</div> <!--32px 64px-->
</div>
<div class="p2">
    <div class="s5">1</div> <!--32px 64px-->
    <div class="s6">1</div> <!--32px 64px-->
</div>
<div class="p3">
    <div class="s5">1</div><!-- 32px 32px -->
    <div class="s6">1</div><!-- 32px 64px -->
</div>

4. margin外边距重叠

margin的计算规则如下,参考:margin - MDN

  • 如果以百分比为单位设置四个方向上的margin值,计算时取的相对于该元素的包含块的宽度与该百分比乘积的结果
  • 元素的margin-top/margin-left被赋予负值时,元素将被拉进指定的方向,覆盖之前的元素
  • 设置margin-bottom/right为负数,元素并不会向下/右移动,而是将后续的元素拖拉进来,覆盖本来的元素
  • margin的top和bottom属性对非替换内联元素无效

collapsing-margin,两个或多个毗邻的普通流中的块元素垂直方向上的 margin 会重叠

假设两个元素box1、box2,其中box1的margin-bottom为mb,box2的margin-top为mt,则发生外边距重叠时,这两个元素之间的距离按照下面标准取舍:

if(mb > 0 && mt > 0){
    // 全部都为正值,取最大者
    ans = Math.max(mb, mt)
}else if(mb < 0 && mt < 0){
    // 全部都是负值,则都取绝对值,然后用0减去最大绝对值
    ans = 0 - Math.max(Math.abs(mb), Math.abs(mt))
}else {
    // 不全是正值,则都取绝对值,然后用正值减去最大绝对值;
    ans = findPositive(mb, mt) - Math.max(Math.abs(mb), Math.abs(mt))
}

下面是一个例子

<style>
    .box1 {height: 200px;background-color: red;margin-bottom: 20px}
    .box2 {height: 200px;background-color: blue;margin-top: 30px}
</style>
<div class="wrap">
    <div class="box1"></div>
    <div class="box2"></div>
</div>

  • mb=20, mt=30时,box1和box2之间的间距为Math.max(20, 30),即30
  • mb=20, mt=-30时,box1和box2之间的间距为正值20 - 最大绝对值30,即-10
  • mb=-20,mt=-30时,box1和box2之间的间距为0 - 最大绝对值30,即-30

5. flex-grow

当容器有剩余宽度时,子元素将按照设置的flex-grow属性比例等分剩余空间。

假设父元素宽度w,三个子元素宽度分别为w1,w2,w3,,三个子元素的flex-grow分别为 a,b,c,则计算公式为

x = w - (w1 + w2 + w3) // 剩余的宽度,分配给子元素

sum = a + b + c // flex-grow 总权重
if(sum < 1){
    sum = 1 // 注意sum小于1时按1计算,剩余空间不会分配
}

第一个子元素可分 x * a / sum,计算后的宽度为 w1 + x * a / sum
第二个子元素可分 x * b / sum,计算后的宽度为 w2 + x * b / sum
第三个子元素可分 x * c / sum,计算后的宽度为 w3 + x * c / sum

下面是一个例子

<style>
    div {height: 200px}
    .f {display: flex;width: 600px}
    .c1 {width: 200px;flex-grow: 1;background-color: red}
    .c2 {width: 300px;flex-grow: 2;background-color: blue}
</style>

<div class="f">
    <!-- 200 + 1/(1+2)*(600-200-300) = 233.33 -->
    <div class="c1"></div>  
    <!-- 300 + 2/(1+2)*(600-200-300) = 366.67 -->
    <div class="c2"></div>
</div>

6. flex-shrink

仅在子元素默认宽度之和大于容器的时候才会发生收缩,其收缩的大小是依据 flex-shrink 的值。

假设父元素宽度w,三个子元素宽度分别为w1,w2,w3,,三个子元素的flex-shrink分别为 a,b,c,则计算公式为

x = (w1 + w2 + w3) - w // 溢出的宽度,由每个子元素收缩一部分

if (a + b + c < 1){
    // 当 flex-shrink 之和小于 1 时,只会收缩一部分宽度,其余仍会溢出容器
    x = (a + b + c) * x
}

sum = a*w1 + b*w2 + c*w3 // flex-shrink总权重,

第一个元素收缩 x * a * w1 / sum 
第二个元素收缩 x * b * w2 / sum 
第三个元素收缩 x * c * w3 / sum 

下面是一个示例

<style>
    div {height: 200px}
    .f {display: flex;width: 600px}
    .c1 {width: 500px;flex-shrink: 1;background-color: red}
    .c2 {width: 400px;flex-shrink: 2;background-color: blue}
</style>

 <div class="f">
    <!--500 -  300 * 1 * 500 / (1*500+2*400) = 384.61 -->
    <div class="c1"></div>  
    <!--400 -  300 * 2 * 400 / (1*500+2*400) = 215.39 -->
    <div class="c2"></div>
</div>

7. calc: 纯css实现rem

参考:calc - MDN

// 假设是750基准的设计图
html {font-size: calc((100vw / 750) * 100);}

@function rem($px) {
    @return 1rem * ($px/100);
}

// 使用方式: rem(40) 40为从设计图量取的尺寸

8. 小结

现在写css时,基本上都只是为了快速完成页面的编写,忽略了很多属性的运行原理和计算规则,只满足于完成任务而已,这是一个很危险的信号。

本文整理了一些常用或容易被忽略的样式计算规则,只有了解了规则的计算原理,才能更好更快地编写合格的样式表。

单调栈及其应用 重新阅读Vue源码