理解CSS布局算法

829 阅读13分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

本文为翻译文章,部分内容难免理解有偏差,如有错误欢迎大家指正。原文链接见文章末尾。

前言

几年前,我对CSS的认识有一个重要的发现。

在那之前,我一直在学习CSS,主要聚焦在我们日常开发时所写的属性和值上面,就如z-index:10或者justify-content:center。我就在想,如果我能够广泛的理解了每个属性的作用,我将会对这门语言有一个更加全面更加深刻的理解。

我的主要理解是,CSS不仅仅是一系列属性的集合。它就像是互相有联系的这些布局算法组成的一个星座。每一个算法就像是一个包含有自身规则的,有其秘密机制的复杂系统。仅仅理解那些特殊属性的作用是远远不够的,我们需要学习布局算法是如何工作的,它们是如何来使用我们设置的这些属性的。

你是否曾经有过在使用熟悉的CSS时感到不安的经历呢,有的之前已经使用过很多次了,但是这次获得的展现结果却是和预期不同呢?这会让人非常的崩溃,让我们觉得这门语言出现了不一致性。完全相同的CSS为什么出现的结果却是不同的呢?

发生上面这种情况的原因是因为这些属性子这个复杂的系统里面产生了作用,而又有一些微妙的上下文环境因素改变了属性的行为。我们对这个语言模型的整体理解是不完整的,就导致给我们带来了“惊喜”。

当我们开始研究布局算法时,一切都变得更有意义。困扰我们很久的问题就会得到解决。我们会意识到CSS实际上是一种非常强大的语言,我们也会真正的开始喜欢去写它。

在这篇文章中,我们将通过这种新的视野来帮助我们理解CSS中发生了什么,以此来揭秘上面这些常见问题的奥秘。

布局算法

那么,什么是布局算法呢?可能有一些你已经很熟悉了,它包括:

  • Flexbox
  • Positioned (eg. position: absolute)
  • Grid
  • Table
  • Flow

从技术上讲,它们称为布局模式,而不是布局算法。但是我发现“布局算法”是一个更有用的标签。

当浏览器呈现我们的HTML内容时,每个元素都将使用默认的布局算法计算其布局。我们可以选择具有特定CSS声明的不同的布局算法来控制它们。例如,使用position: absolute来切换元素成为绝对定位布局。

让我们来看一个例子,比如说有以下CSS样式:

.box {
  z-index: 10;
}

我们第一步先要确认将会使用哪种布局算法来渲染box元素。基于上面的CSS,我们知道使用的是流式布局(Flow)的方式。

流式布局可以算是最原始的布局,它是在互联网被视作一组巨大的超链接文档的年代创建的。它有点类似于Microsoft Word等文字处理软件中使用的布局算法。

流式布局用于非表类HTML元素的默认布局算法。除非我们特意去更改使用的布局算法,不然将会默认使用流式布局。

z-index属性用于控制元素的堆叠顺序,以确定如果元素出现重叠的时候哪一个元素在“顶部”。事实上:它没有在流式布局里面实现,流式布局是创建文档流风格的布局方式,我们还没有见过有允许元素重叠的文字处理软件。

如果您几年前问我,我会这样回答:

您不能在没有设置定位是“相对”或者“绝对”定位的情况下使用z-index,因为z-index属性依赖于定位方式。

这并不是完全错误的,但是这里面有一些误解。更准确的说,z-index属性未在流式布局算法中实现。因此,如果我们希望这个属性有效的话,我们需要选择其他的布局算法。

可能您感觉我有点卖弄学问,但是这种小的误解可能会导致大的困惑。比如,看下面的例子:

在这个例子里面,我们使用Flex布局设置了3个兄弟节点。中间节点我们设置了z-index并生效了。尝试去掉它,可以注意到它到其他节点的后面了。怎么会这样呢?我们也没有设置position: relative啊!

之所以可以生效,是因为Flexbox算法中实现了z-index属性。当语言作者在设计Flexbox算法时,他们决定将z-index属性设置来控制堆叠顺序,就像它在定位布局中一样。

这是关键的心理模型的转变。 CSS属性本身毫无意义,布局算法定义了它们的作用,它们是如何在布局计算中被使用的。

需要明确的是,还是有一些CSS属性在所有的布局算法中呈现的作用都是相同的。例如color: red无论如何都会让文字显示成红色。但是,每种布局算法都可以覆盖掉任何属性的默认行为,,而有些属性是没有默认行为的。

下面的例子是一个让我惊讶的例子:你是否知道宽度属性会根据布局算法的不同而实现不同的效果呢?举例证明下:

.item元素只有一个CSS属性width: 2000px,它的第一实例是在流式布局渲染的,实际上它也的确是2000px的宽度。在流式布局中,width属性是一个“严格”的属性,设置后它就会占用2000px宽度的空间。第二个实例,渲染在Felx容器中,意味着将会按照Flexbox布局来展现,在Flexbox算法中,width更像是一个“建议”。Flexbox规范将其称为假设的大小,元素的规模没有任何强制的限制或作用。在适当的范围内,它将会是2000px宽度的,但是如果放置到一个较小的容器中,它将会被收缩以容纳进去。

再重申一次,框架起到的作用是非常重要的。在Flexbox布局中,并不是说width属性是特殊的。只是说Flexbox布局算法和流式布局算法实现width属性的方式是不同的。

我们写的CSS属性是输入,就像我们传递给函数的参数一样。由布局算法来确定选择哪种处理方式来处理这些输入内容。如果我们想理解CSS,我们就需要理解这些布局算法是如何工作的,仅仅知道这些属性值的使用是不够的。

确定布局算法

CSS没有布局模式的属性供我们设置,但是有几个属性可以供我们调整使用的布局算法,并且在实际使用中有时候会让我们感觉棘手。

在一些情况下,应用于元素的CSS属性将会使之进入特定的布局模式。例如:

.help-widget {
  /* 使用定位布局 */
  position: fixed;
  right: 0;
  bottom: 0;
}
.floated {
  /* 使用浮动布局 */
  float: left;
  margin-right: 32px;
}

在其他情况下,我们需要查看元素的父元素适用哪种布局:

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

当我们使用display: flex时,我们实际上并没有为.row元素适用Flexbox布局;取而代之的是,我们要说的是,它的子元素应该使用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>

这三个列表项目均为弹性布局容器中的子元素,因此应该根据Flexbox布局进行定位。但是中间元素又设置了position: absolute使之进入了定位布局。按照我的理解,一个元素的渲染会按照一个主要的布局模式来确定,某些布局模式优先级会比其他布局模式更高。我不清楚确切的层次结构,但是定位布局往往会击败一切(可能是因为定位布局影响了文档流布局结构)。因此,在上面的例子中,中间元素将会按照定位布局来渲染,而不是Flexbox布局。结果就是,Flexbox布局计算只适用于两个元素,而不是三个。就Flexbox布局而言,中间元素是不存在的,它不影响布局算法。

通常,出现冲突是我们有意为之,但是如果发现一个元素没有按照期望的方式来渲染的话,分析确定它正在使用哪种布局方式是非常必须要的。答案有可能会让你感到惊讶!

❗️ 相对定位(Relative Positioning)

有一个难题是:如果每个元素通过单一的布局算法进行渲染的话,我们怎么理解相对定位呢? 元素设置了position: releative之后很明显的应该通过定位布局进行渲染。它可以设置专门的定位布局属性top,left属性,但是,它也可以参与到Flexbox/网格布局中。在学习本文前可能这个问题比较复杂,但是现在我们可以这样解释:

每个元素都在特定的格式上下文中渲染,这取决于布局算法来决定是否参与其中。通常,定位布局算法会忽略此类上下文,但它也为相对定位奠定了例外。

当在Flexbox上下文中渲染相对定位的元素时,定位布局算法将允许它参加进去。一旦使用该上下文建立大小/位置时,它就会应用定位布局内容(例如通过topleft调整位置)

可以把它理解成一种组合,定位布局算法和Flexbox布局算法可以组合作用到相对定位的元素上。

内联魔术空间

好吧,让我们看一个经典的“迷惑CSS”问题,看看如何专注于布局算法可以帮助我们解决它。

嗯...为什么图像下面有一些额外的空间?如果使用开发者工具查看的话,会发现一些像素上的差异:视频演示

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

如果你熟悉盒子模型,你会知道可以使用padding,border,margin进行填充。您可能会认为图像上有一些margin设置,或者是在容器上有padding设置?在这个例子中,这些属性均不生效。这就是为什么这么多年来我一直将其称为“内联魔术空间“。它不是元凶。要理解这里发生的事情,我们必须深入研究流式布局。

流式布局 Flow Layout

如前所述,流式布局是为文档流设计的,类似于文字处理软件那样。文档具有以下结构:

-> 单个字符被组装成单词和句子。这些元素设置成inline,并排着,当水平空间不够时,将进行换行处理

-> 段落被视为,例如标题或者图片。块会被垂直堆叠,一个在另一个上方,从上到下排列。

流式布局基于此结构,单个元素可以作为内联元素(并排的的段落中的单词),或块元素(从上到下堆叠)

1654515555026.png

1654515561553.png

方向因语言而异

在这个示例中,我们假设正在使用像是英语这样的水平方向,从左到右的语言。事实并非全部如此。某些语言,例如阿拉伯语和希伯来语,是水平方向从右到左写的。其他类似于中文,古时候都是从上到下垂直写的。

大部分HTML元素都有默认值。<p> <h1>被认为是块级元素,<span> <strong>被视为内联元素。内联元素应该在段落中使用,而不是布局的一部分。例如,我们想要在句子中间添加一个小图标。为了使内联元素不会对周围文本内容的可读性产生负面影响,一些额外的垂直空间需要添加上。这就是上面我们的例子中为什么图像会多出一些额外的空间。因为默认情况下,图像是内联元素。

流式布局算法将此图像视为段落中的字符,并且在下面添加一些空间,以确保它不会影响和下一行内容的展示情况。默认情况下,内联元素是“基线”对齐的。这意味着图像的底部将和文本坐落的水平线保持一致。这就是为什么图像下方有一些空间的原因-这个空间为了适应后代,例如字母jp

所以,它不是因为margin,padding,border,这就是流式布局适用于内联元素的固有空间。

解决上面的问题

有多种方法可以解决上面的问题,最简单的方法是将图像看作块级:

或者将布局方式改为其他的布局模式

我们还可以通过使用line-height将额外空间压缩到0来解决

该解决方案通过将其设置为0来删除所有其他行间距,这会使多行文本的阅读性收到影响,但是本例不包含文本,所以不算是问题。

结语

CSS中有很多布局算法,它们都有自己的怪癖和隐藏的机制。当我们专注于CSS属性时,我们只看到冰山一角。我们从不了解诸如堆叠环境或包含块或级联起源之类的真正重要概念!CSS是一种棘手的语言。我们没有错误提示或调试器或Console.Log。我们的直觉是我们拥有的最好的工具。而且,当我们仅仅使用CSS片段而不真正理解它们时,出现疑惑只是时间问题。所以我们需要在日常对CSS属性熟练使用的过程中,去深入理解不同的布局算法,这样才能为我们避免一些意想不到的问题。


原文链接:

Understanding Layout Algorithms

补充理解资料:

CSS 流式布局

CSS 定位布局

前端重构范式之浮动布局-Float Layout