美文网首页
理解CSS布局算法

理解CSS布局算法

作者: 涅槃快乐是金 | 来源:发表于2024-05-22 21:08 被阅读0次

    CSS远不止是一组属性的集合。它是一系列相互关联的布局算法的集合。每个算法都是一个复杂的系统,有着自己的规则和秘密机制。

    仅仅了解特定属性的作用是不够的。我们需要学习布局算法是如何工作的,以及它们如何利用我们提供的属性。

    你有没有经历过这样的疑惑:写下一个熟悉的CSS代码块,以前用过很多次,但最终得到了不同和意想不到的结果?这真的很令人沮丧。它让这门语言感觉不一致和靠不住。为什么完全相同的CSS输入会产生不同的输出呢?

    这种情况发生是因为属性作用于一个复杂的系统,有一些微妙的上下文会改变属性的行为方式。

    布局算法

    那么,“布局算法”是什么?你可能已经对其中一些很熟悉了。它们包括:

    • Flexbox
    • 定位(例如position: absolute)
    • 网格
    • 表格
    • 流动

    (严格来说,它们被称为布局模式,而不是布局算法。)

    当浏览器渲染我们的HTML时,每个元素的布局都将使用主要的布局算法计算。我们可以通过特定的CSS声明来选择不同的布局算法。例如,应用position: absolute将使一个元素切换到使用定位布局。

    让我们看一个例子。假设我有以下CSS:

    .box {
      z-index: 10;
    }
    

    我们首先要做的是弄清楚哪种布局算法将被用于渲染.box 元素。根据提供的CSS,它将使用Flow layout进行渲染。

    Flow layoutWeb的“原生”布局算法。当时Web主要被视为一个巨大的超链接文档集,就像世界上最大的档案馆一样。它类似于字处理软件(如Microsoft Word)中使用的布局算法。

    Flow layout是非表格HTML元素的默认布局算法。除非我们明确选择另一个布局算法,否则将使用Flow layout

    z-index属性用于控制堆叠顺序,以确定哪一个显示在“顶部”如果它们重叠在一起。但事实是:它在Flow layout中并未实现。Flow layout完全是关于创建文档样式的布局,从未见过允许元素重叠的文字处理软件。

    如果几年前有人问我这个问题,我可能会说类似于:

    "你不能只设置z-index,而不设置position为类似“relative”“absolute”,因为z-index属性依赖于position属性。"

    这并不完全错误,更准确的说法是,z-index属性在Flow layout算法中没有实现,因此如果我们希望该属性生效,我们需要选择一个不同的布局算法。
    考虑这个例子:

    <style>
      .row {
        display: flex;
        gap: 16px;
      }
      .raised.item {
        z-index: 2;
        background: hotpink;
      }
    </style>
    
    <ul class="row">
      <li class="item"></li>
      <li class="raised item"></li>
      <li class="item"></li>
    </ul>
    

    我们有3个兄弟元素,使用Flexbox布局算法排列。

    中间的兄弟元素设置了z-index,并且它起作用了。尝试移除它,注意到它会放置于其兄弟元素后面。

    这是怎么回事呢?我们哪里都没有设置position: relative

    这是因为Flexbox算法实现了z-index属性。设计Flexbox算法时,他们决定将z-index属性与控制堆叠顺序联系起来,就像在定位布局中一样。

    这是关键的心智模型转变。CSS属性本身是没有意义的。布局算法决定了它们的作用,以及它们在计算中的使用方式。

    要清楚,有一些CSS属性在所有布局算法中都是相同的。例如,color: red将产生红色文本。但是每个布局算法都可以覆盖任何属性的默认行为。而且许多属性没有任何默认行为。

    这里有一个让我震惊的例子:你知道吗,width属性的实现取决于布局算法的不同吗?

    证明如下:

    <style>
      .flex-wrapper {
        display: flex;
      }
      .item {
        width: 2000px;
      }
    </style>
    
    <div class="item"></div>
    
    <div class="flex-wrapper">
      <div class="item"></div>
    </div>
    

    我们的.item元素只有一个CSS属性:width: 2000px

    第一个.item 实例使用流动布局渲染,它实际上将占据2000px的宽度。在流动布局中,width是一个硬性规则。它将占用2000px的空间。

    然而,第二个.item 实例被渲染在一个Flex容器中,这意味着它使用的是Flexbox布局。在Flexbox算法中,width更像是一个建议。

    Flexbox规范将其称为假设大小。这是元素在理想世界中的大小,在没有约束或力量作用的情况下。在一个完美的世界中,这个项目将是2000px宽,但它被放置在一个较窄的容器中,所以它会收缩以适应它。

    再次强调,这里的框架非常重要。它不是说在Flexboxwidth有一些特殊的附加条件。而是Flexbox算法以一种不同的方式实现了width属性。

    我们编写的属性就像传递给函数的参数一样是输入。布局算法决定如何处理这些输入。如果我们想理解CSS,我们需要了解布局算法是如何工作的。仅仅知道属性是不够的。

    识别布局算法

    CSS没有一个布局模式(layout-mode)属性。有几个属性可以调整使用的布局算法,实际上这可能会变得相当棘手!

    在某些情况下,应用于元素的CSS属性将选择特定的布局模式。例如:

    .help-widget {
      /* 使用了定位布局,因为有这个声明: */
      position: fixed;
      right: 0;
      bottom: 0;
    }
    .floated {
      /* 使用了浮动布局,因为有这个声明: */
      float: left;
      margin-right: 32px;
    }
    

    在其他情况下,我们需要查看父级应用的CSS。例如:

    <style>
      .row {
        display: flex;
      }
    </style>
    <ul class="row">
      <li class="item"></li>
      <li class="item"></li>
      <li class="item"></li>
    </ul>
    

    当我们应用display: flex时,我们实际上并未使用 Flexbox 布局算法来渲染 .row 元素;相反,我们是在说它的子元素应该使用 Flexbox布局来定位。

    从技术上讲,display: flex 创建了一个flex格式化上下文。所有直接子元素都将参与此上下文,这意味着它们将使用 Flexbox布局,而不是默认的流动布局。

    display: flex 也会将内联元素,如 <span> 转换为块级元素,所以它对父元素的布局确实有一些影响。但它不会改变使用的布局算法。)

    布局算法变体

    一些布局算法分为多个变体。

    例如,当我们使用定位布局时,这涉及到几种不同的“定位方案”:

    • 相对定位(Relative)
    • 绝对定位(Absolute)
    • 固定定位(Fixed)
    • 粘性定位(Sticky)

    每个变体都有着自己的小型布局算法,尽管它们也有一些共同之处(例如,它们都可以使用z-index属性)。

    同样地,在流动布局中,元素可以是块级或内联的。我们稍后会更多地讨论流动布局。

    冲突

    当一个元素应用了多个布局算法时会发生什么?

    例如:

    <style>
      .row {
        display: flex;
      }
      .primary.item {
        position: absolute;
      }
    </style>
    <ul class="row">
      <li class="item"></li>
      <li class="primary item"></li>
      <li class="item"></li>
    </ul>
    

    所有三个列表项都是Flex容器内的子元素,所以它们应该根据 Flexbox 进行定位。但是中间的子元素通过设置 position: absolute 来选择定位布局。

    据我理解,一个元素将使用主要的布局模式进行渲染。这有点像特异性:某些布局模式的优先级比其他模式更高。

    我不知道确切的层次结构,但是定位布局通常胜过其他一切。因此,在这个例子中,中间的子元素将使用定位布局,而不是Flexbox

    因此,Flexbox计算将表现得好像只有两个子元素,而不是三个。就 Flexbox算法而言,中间的子元素不存在。

    一般来说,冲突通常是非常明显的/有意的。但是,如果您发现某个元素的行为不像您期望的那样,那么就值得尝试确定它使用的是哪种布局算法。答案可能会让你大吃一惊!

    Inline space

    这里有一篮子猫:



    为什么图像下方有一些额外的空间?

    如果你用开发者工具检查它,你会发现有几个像素的差异:

    图像是250px高,但容器是258.5px高!

    如果你熟悉盒模型,你知道元素可以通过填充、边框和外边距进行间隔。你可能会认为图像有一些外边距,或者容器有一些填充?

    在这种情况下,这些属性都不是原因。这就是为什么多年来,我私下称之为“Inline space”。它不是由通常的罪魁祸首引起的。

    要理解这里发生了什么,我们必须深入了解流布局。

    流布局

    如前所述,流布局是为文档设计的,类似于文字处理软件。

    文档具有以下结构:

    • 单个字符组装成单词和句子。这些元素是内联的,并排放置,当没有足够的水平空间时会换行。
    • 段落被视为块级元素,如标题或图像。块会垂直堆叠,从上到下排列。

    流布局基于这种结构。单个元素可以排列为内联元素(并排放置,如段落中的单词),或作为块级元素(如从上到下堆叠的砖块).

    大多数 HTML 元素都有合理的默认值。<p><h1> 被视为块级元素,而 <span><strong> 被视为内联元素。

    内联元素旨在用于段落中,而不是布局的一部分。例如,也许我们想在句子中间添加一个小图标。

    为了确保内联元素不会对周围文本的可读性产生负面影响,会添加一些额外的垂直空间。

    所以,回到我们的谜题:为什么我们的图像有一些额外的像素空间?因为图像默认是内联元素!

    流布局算法将这个图像视为段落中的一个字符,并在其下方添加一些空间,以确保它与(理论上的)下一行字符不会过于接近。

    默认情况下,内联元素是“基线”对齐的。这意味着图像的底部将与文本所在的看不见的水平线对齐。这就是图像下方有一些空间的原因——那个空间是为了像字母jp这样的下降部。

    所以,这不是外边距、填充或边框……这是流布局对内联元素应用的一点内在空间。

    解决问题

    有多种方法可以解决这个问题。也许最简单的方法是将此图像视为块元素,在流布局中:

    <style>
      .cat-photo {
        display: block;
      }
    </style>
    
    <div class="photo-wrapper">
      <img
        class="cat-photo"
        alt="一篮子猫"
        src="/images/cats.jpg"
      />
    </div>
    <div class="photo-wrapper">
      <img
        class="cat-photo"
        alt="一篮子猫"
        src="/images/cats.jpg"
      />
    </div>
    

    或者,因为这种行为是流布局独有的,我们可以切换到另一种布局算法:

    <style>
      /*
        我们将其*父元素*设置为 Flex,
        这样子元素将使用
        Flexbox 而不是 Flow:
      */
      .photo-wrapper {
        display: flex;
      }
    </style>
    
    <div class="photo-wrapper">
      <img
        class="cat-photo"
        alt="一篮子猫"
        src="/images/cats.jpg"
      />
    </div>
    <div class="photo-wrapper">
      <img
        class="cat-photo"
        alt="一篮子猫"
        src="/images/cats.jpg"
      />
    </div>
    

    最后,我们还可以通过将额外的空间缩小到0来解决这个问题,使用 line-height

    <style>
      .photo-wrapper {
        line-height: 0;
      }
    </style>
    
    <div class="photo-wrapper">
      <img
        class="cat-photo"
        alt="一篮子猫"
        src="/images/cats.jpg"
      />
    </div>
    <div class="photo-wrapper">
      <img
        class="cat-photo"
        alt="一篮子猫"
        src="/images/cats.jpg"
      />
    </div>
    

    这种解决方案通过将所有额外的行间距设置为0来消除它。这会使多行文本完全不可读,但由于此容器不包含文本,因此这不是问题。

    我会推荐使用前两个解决方案之一。这个方案只是为了证明问题确实是由行间距引起的!

    行高
    在谈到行高时:你知道未经样式化的HTML实际上被认为是不可访问的吗,因为行距太近了?对于有阅读障碍的人来说,当行距不够大时,很难解析文本。
    大多数浏览器默认行高为1.11.2,但根据 WCAG指南,我们应将其设置为至少1.5用于正文文本。

    孰能生巧

    如果你只专注于研究特定的 CSS 属性做什么,你永远不会理解这个神秘的空间从何而来。它在 MDN页面上的 displayline-height 中没有解释。

    “内联魔法空间”其实并不是真正的魔法。它是流布局算法中的一个规则造成的,即内联元素应该受到行高的影响。但对我来说,这似乎是魔法,因为我的心智模型中有一个很大的漏洞。

    CSS中有很多布局算法,它们都有自己的怪癖和隐藏机制。当我们专注于CSS属性时,我们只是看到了冰山一角。我们从未学习过真正重要的概念,比如堆叠上下文、包含块或层叠来源!

    CSS是一种难以调试的语言;我们没有错误消息、调试器或 console.log。我们的直觉是我们最好的工具。当我们开始使用CSS而不真正理解它们时,总有一天,布局算法的某些隐藏方面会打乱我们的计划,让我们陷入困境。

    相关文章

      网友评论

          本文标题:理解CSS布局算法

          本文链接:https://www.haomeiwen.com/subject/wkwzfjtx.html