从一个垂直居中的方法深度了解基线与 vertical-align

4,094 阅读8分钟

案例:采用伪元素实现垂直居中

今天在一篇文章中无意看到一个利用伪元素实现垂直居中的方法,费了好大劲理解了它的原理,于是就有了这篇文章。

这个方法是通过在父元素上添加一个高度 100%、vertical-align: middle的伪元素实现垂直居中的,效果和代码如下:

<div class="parent">
  <div class="child">child</div>
</div>
.parent {
    width: 300px;
    height: 300px;
    border: 1px solid red;
    text-align: center;
}
.child {
    background: blue;
    width: 100px;
    height: 40px;
    display: inline-block;
    vertical-align: middle;
}
.parent::before {
    content: '';
    height: 100%;
    display: inline-block;
    vertical-align: middle;            
}

基本概念

在深入了解之前,先来了解一些基本概念。

基准元素

一行 inline 元素中,取行高最高元素的作为基准元素。

行内元素的基线

我们都知道,默认情况下,行内元素的垂直对齐方式为基线对齐。这里的基线(base line)指的是英文字母“x”的下端沿:

图片来源:https://blog.csdn.net/lulujiajiawenwen/article/details/8245201

举个例子,下图的两个行内元素都是基线对齐的,红色的线就是它们的基线:

<div class="parent">
  <div class="child1">abcxyz</div>
  <div class="child2">efghij</div>
</div>
.parent {
  width: 300px;
  height: 100px;
  border: 1px solid blue;
}
.child1 {
  display: inline-block;
  height: 20px;
  background-color: green;
}
.child2 {
  display: inline-block;
  height: 20px;
  background-color: yellow;
}

行内元素垂直对齐的依据:vertical-align 属性

行内元素是如何判断垂直对齐的位置呢?是依据 vertical-align属性。按照 w3school 上的解释:该属性定义行内元素的基线相对于该元素所在行的基线的垂直对齐方式,默认值为 baseline。

深入了解

辨别“行内元素的基线”与“行内元素所在行的基线”

再看一遍 vertical-align 的概念:该属性定义行内元素的基线相对于该元素所在行的基线的垂直对齐方式。这里有两个词组:什么是“行内元素的基线”?什么是“该元素所在行的基线”?

毫无疑问,“行内元素的基线”就是我们在基线中所说的:字母“x”的下端沿。我们在一个元素里写一个"x",它的下沿就是这个元素的基线。

可以看到,元素的基线和元素的底线之间是有间距的(上图"x"和红色底边之间),这部分留给"g"、"j"等字母或中文字符

那么什么是“该元素所在行的基线”?一句话概括就是:所在行的基准元素在对齐时所依据的那条线。我们把该元素所在行当作父元素,这句话表明,父元素的基线是由它的一个子元素——基准元素决定的,基准元素依据哪条线对齐,父元素的基线就是哪条线

这时我们就明白了这两个“基线”的区别:

  • 行内元素的基线:是它的自有属性,当该元素没有子元素而只有文字时,取字母"x"的下端沿
  • 行内元素的父元素的基线:来自于它的一个子元素——基准元素,基准元素依据哪条线对齐,父元素的基线就是哪条线

也就是说:如果基准元素采用中线对齐,那么所在行的基线就是基准元素的中线;如果基准元素采用顶线对齐,那么所在行的基线就是基准元素的顶线。

再看垂直对齐的依据与过程

有了上面的铺垫,我们再回过头来看一下这句话:默认情况下,行内元素的垂直对齐方式为基线对齐。那么,如何根据这句话解释下面的两个元素在垂直方向上的对齐过程呢?

.parent {
  width: 300px;
  height: 100px;
  border: 1px solid blue;
}
.child1 {
  display: inline-block;
  width: 50px;
  height: 100%;
  background-color: green;
}
.child2 {
  display: inline-block;
  width: 50px;
  height: 20px;
  background-color: yellow;
}

可能有人会说,这很简单啊,两个元素都没有设置 vertical-align 属性,那么按照默认情况,它们都是基线对齐,所以内部文字就都沿着红线对齐了。

这句话错在哪儿?错在“都是”。正确过程应该是:基准元素按照自己的基线对齐,其余行内元素按照父元素的基线对齐:

  1. 父元素首先确定基准元素,即最高的元素 —— child1
  2. child1vertical-align 属性默认值为 baseline,因此它对齐自己的基线 —— 作为基准元素,位置并没有发生变化,但是会影响父元素的基线位置
  3. 于是父元素的基线就是 child1 的基线
  4. child2vertical-align 属性默认值为 baseline,因此它的垂直对齐方式也是“基线对齐”
  5. 但是最重要的一点是: child2按照父元素的基线对齐,而不是按照自己的基线对齐,即 child2 的基线对齐父元素的基线

似乎看起来两种解释最后都没啥区别啊?为什么一定要这么大费周章地讲一遍对齐过程呢?现在让我们给 child1 添加 vertical-align: middle,想一想,child2的位置在哪儿呢?

咦?给 child1 添加 vertical-align: middlechild2 就自动“垂直居中”了?!可是我没有给 child2 设置 vertical-align: middle啊,这是为什么?

按照我们之前的过程再分析一遍,就简单多了:

  1. 父元素首先确定基准元素,即最高的元素 —— child1
  2. child1 这次采用 vertical-align: middle 中线对齐 —— 作为基准元素,位置并没有发生变化,但是会影响父元素的基线位置
  3. 于是父元素的基线就是 child1 的中线
  4. child2vertical-align 属性默认值为 baseline,因此它的垂直对齐方式也是“基线对齐”
  5. child2 按照父元素的基线对齐,所以就是它的基线要对齐 child1 的中线

那这里我们又要思考,child2 真的垂直居中了吗?答案是没有。因为我们说“ child2 的基线对齐的是 child1 的中线”,所以 child2 并没有垂直居中,而是在中部靠上一点点的位置。

现在我们给 child2 添加 vertical-align: middle,会发现它下降了一点点,这时候它就真正的垂直居中了:

我们再分析一下这个过程:

  1. 前三步都一样:父元素的基线是 child1 的中线
  2. child2vertical-align 属性为 middle,采用中线对齐,所以就是它的中线对齐父元素的基线,又或者,相当于它的中线对齐 child1 的中线

总结

到这时,相信你对于浏览器如何实现垂直对齐,已经有一个较为深入的理解了。你可以修改 child1child2vertical-align 属性为其他值(suptop、...),看看会发生什么,并试着用以上过程解释一下原因。

  1. 父元素的基线 === 基准元素的 vertical-align 属性对应的线
  2. 每个行内元素将自己 vertical-align 属性对应的线对齐到父元素的基线

案例原理

伪元素的插入位置

首先,before 伪元素渲染在父元素的所有子元素之前,after 伪元素插入在父元素的所有子元素之后。也就是说伪元素和子元素是同级的。

原理

回到我们文章开头所说的案例,原理呼之欲出:利用content: ''; height: 100% 得到一个宽度为 0 的伪元素,即它不会显示出来;高度 100%,所以它最高,是所在行的基准元素;利用 vertical-align: middle 将父元素的基线设置为伪元素的中线,然后其他行内元素采用中线对齐时,自然就是要对齐伪元素的中线了。而伪元素的高度为 100%,所以它的中线就是整个父元素的中线,这就实现了其他行内元素的垂直居中。

我对底层原理的猜想

从编译原理的角度出发,我这样理解上面的过程:

浏览器在解析 CSS 的时候,对于一个行内元素:

  • 如果这个行内元素没有任何子元素,即只有文字或是一张图片,那么它的 vertical-align 属性是一个固有属性
  • 这个行内元素的父元素的基线是一个综合属性,归约而得:它先找到自己所有子元素里最高的那个子元素,然后取它的 vertical-align 属性对应的线作为自己的基线

然后把所有的行内元素,依据 vertical-align 属性,对齐到父元素的基线上。

这也解释了为什么设置基准元素的 vertical-align 属性时,基准元素位置不变,因为父元素的基线本来就是根据它的 vertical-align 属性值取的

结束语

以上就是我对 vertical-align 和基线的理解,欢迎各位大佬批评指正。