06月19, 2017

CSS 中不为人知的部分

原文地址:https://madebymike.com.au/writing/the-invisible-parts-of-CSS/

如果你在日常工作中使用 CSS,你的主要目标可能会重点围绕着使事情“看起来正确”。如何实现这一点经常是远不如最终结果那么重要。这意味着比起正确的语法和视觉结果来说,我们更少关心 CSS 的工作原理。

CSS 的视觉结果通常是操作隐藏属性的间接后果,你可能还没有意识到这一点。某些 CSS 属性(比如 background-color)与你看到的内容有直接而明显的关系。同时,其它像 display 这样的属性对于我们很多人来说仍然是模棱两可的,因为结果似乎与上下文环境有很大关系。

我怀疑很多开发人员都不能用简单的术语描述设置 display: block 实际上是在做什么,顶多可能对这种属性如何工作有一些直观的了解。没关系,你可以在不了解底层原理的情况下,就能 CSS 瞎嚷嚷。不过,那也是只知其一,不知其二。

如果这就是在说你,没关系。我也是在学会如何使用 CSS 很久以后,才理解 CSS 的工作原理。我猜这并不能让你好受点,不过至少你并不是一个人在战斗!

CSS 的底层特性是复杂而有意抽象的,但我们也不能完全不了解它们。对于我们很多人来说,或多或少都会熟悉像盒模型、层叠和特殊性这些概念。虽然它们经常被曲解,但是懂点这些概念的工作原理有助于我们编写更好的 CSS。

对于 CSS 的很多其它隐藏部分也可以这样说。问题是更好地理解这些部分的门槛更高。经常会感觉好像没有什么能孤立地解释。需要先了解所有内容,然后才能理解流程的最小部分。

因此,我想尝试揭示一些 CSS 不为人知的部分,仅触及你需要知道的内容,并希望以逻辑顺序解释该过程,以便更好地理解 CSS 的实际工作原理。

这是一篇很长的文章,所以如果你想跳过这些部分之一,我完全可以理解。

渲染过程概述

当加载一个 HTML 文档时,为了让该页面渲染,有很多事情要发生。

第一个步骤就是解析 HTML 文档。浏览器从这一步创建一个文档树。树状结构是表示像 HTML 这种具有明显层次结构信息的一种方法。树中的元素可以被描述为类似于族谱,比如祖先、父亲、孩子和兄弟姐妹。

你可能听说过术语 DOM。它代表文档对象模型(Document Object Model)。这是文档树结构的一种扩展,被用于存储和操作 Web 文档内容有关的信息。

随着 HTML 被解析,样式表和其它资源也被获取。样式声明通过一个称为层叠的过程来解释和确定。

在此过程期间,CSS 属性的最终值被确定。在计算后,这些值可能与样式表中定义的有所不同。例如,像 auto 这种关键字和相对单位被指派了真实值,并且继承值被应用了。这些计算值被存储在一个树中,类似于 DOM 中的元素,毫不奇怪,它被称为 CSS 对象模型或者 CSSOM。

现在才有可能开始渲染页面的过程。这个过程的第一步是盒模型的计算。这是一个算出元素的尺寸和间距的重要步骤,虽然不一定是元素的最终位置。

与盒模型相比,不那么被人熟知的是一个称为视觉格式化模型的过程。这个过程确定元素在页面上的布局和位置。它包含了一些你可能已经熟悉的概念,比如定位方案、格式化上下文、显示模式和堆叠上下文。

最后,页面被渲染。

上面的段落中可能有一些你还不熟悉的一些术语。如果是这样的话,最重要的是要理解层叠、盒模型以及视觉格式化模型。这些术语都是解释、处理和渲染 HTML 和 CSS 的核心步骤。在描述这些术语时,我跳过很多细节,所以我们下面要更详细地研究一下这三个步骤。

层叠

层叠可能是最被误解的 CSS 特性之一。它是指组合不同样式表以及解决 CSS 选择器之间冲突的过程。

层叠查看声明的重要性、来源、特殊性以及顺序,来确定使用哪个样式规则。

你需要知道什么:

大多数网站有多个样式表。通常,样式是用引用一个 CSS 文件的一个 link 标记,或者 HTML 主体部分的 style 标记添加的。即使最基础的页面也会有浏览器提供的默认样式。这种默认样式表有时被称为 user-agent 样式表。

在层叠期间,样式表是按如下顺序被解释的:

  1. !important 声明

  2. 作者的样式表

  3. 浏览器默认的样式表

注意:这里我跳过了用户样式表,是因为它们不再常见,可能不会是读这本文的所有人要考虑的因素。

在组合这些来源后,如果多个规则应用到同一元素,就用特殊性来确定应用哪条规则。

特殊性

特殊性(Specificity)是给选择器的权重。它常被误认为是一个数字。实际上是 4 个单独的数字或者 4 种权重类别。

要计算特殊性,要统计如下选择器的数目:

  1. ID 选择器

  2. 类选择器、属性选择器和伪类选择器

  3. 元素选择器和伪元素选择器

例如:#nav .selected:hover > a::before 将是 1, 2, 2

无论有多少个类选择器,都不会比一个 ID 选择器有更高的特殊性。当比较选择器时,我们首先比较 ID 选择器的特殊性。只有 ID 选择器相等时,才比较类选择器、属性选择器和伪类选择器的特殊性值。如果最后依然相等,才比较元素和伪元素选择器。

如果每个类别的特殊性都是相等的,那么来源中最后的声明获胜。

是的!我知道我说的是 4 个类。行内样式比 ID 选择器有更高的特殊性。不过由于在技术上行内样式在特殊性计算中是第一类,通常你不会与行内样式竞争,所以很容易记住行内样式的特殊性总是会占优。

重要的注意事项:虽然 !important 声明不会作为特殊性计算的因素,不过它们比层叠中的普通声明有更大的优先级。

继承

继承不是层叠的一部分,不过我在这里把它包含进来,是因为它经常被与层叠结合在一起讨论。

继承是应用给一个元素的值可以被传递到(或者继承)子元素上的过程。

你肯定知道这个事实,即当把字体属性应用到 body 或者其它容器元素时,该属性也会被容器内的所有元素所继承。这就是继承。

并非所有属性默认都被继承。理解继承是编写更优雅更简洁 CSS 的关键。有时候用 inherit 关键字强制继承会相当有用。

注意: 有些属性(比如 border-color)默认值是 currentcolor。也就是说,它们会使用在 color 属性上设置的值。默认值与继承不是一码事。不过,color 属性本身经常是被继承的,所以我倾向于认为这是一种事实上的继承。

盒模型

理解盒模型是布局和定位时防止挫败所必需的。盒模型是 CSS 中最基本的概念之一。

盒模型用于计算元素的宽度和高度。它只是计算过程中的一个步骤,确定元素的最终布局和定位并非完全依赖于它。

你需要知道什么:

HTML 中的每个元素都是一个矩形的盒子。每个盒子有用元素的外边距(margin)、边框(border)、内边距(padding)和内容区定义的四个区域。

默认情况下,当我们设置一个元素的宽度时,只是设置内容区的宽度。当给一个元素添加内边距、边框或者外边距时,是增加了除宽度以外的部分。实际上,这就是说宽度为 50% 的两个元素如果添加了内边距、外边距或者边框,就不会并排填满宽度(因为已经超过了 100% 宽度)。

shows the content-width within the box model

就是这样!相当简单对吧?那么为什么这经常是困惑的来源呢?好吧,你可能已经遇到过一些事情好像表现得有点不同的情况。。。

填充区

当设置元素的背景时,不仅会填充内容区,还会填充内边距区和边框区。

shows the fill area within the box model

概念上,我们把一个 HTML 元素当作是一个东西,所以很容易认为一个元素的视觉边界等于它的宽度,不过实际却并非如此。虽然一个元素的视觉边界包含了内边距和边框区,不过 width 属性是显式地被应用到内容盒。

注意: 更改 box-sizing 属性可以改变这种行为。

Width Auto

另一个潜在的困惑来源是 width: auto 的工作机制。宽度为 auto 是大多数 HTML 元素的默认元素,对于像 div、p 这种块元素来说,auto 会计算宽度,这样外边距、边框、内边距以及内容区都组合在一起也能刚好放在可用空间内。

在这种情况下,感觉添加内边距和外边距会向内挤压内容,不过实际上,宽度会被重新计算,以确保所有东西都能刚好放下。相比之下,在设置宽度为 100% 时,光内容区就会把可用空间填满,而不管外边距、内边距和边框。

shows how width auto works compared to width:100%

Box-sizing

box-sizing 属性会改变盒模型的工作方式。当 box-sizing 被设置为 border-box 时,内边距和边框会减少内容区的内部宽度,而不是添加元素的整体宽度。也就是说,元素的宽度现在与其视觉宽度是相同的。

shows how box-sizing:border-box works

很多人喜欢这个属性,而且如果你正在创建栅格系统,或者其它需要水平对齐条目的任何布局类型,这个属性就是更直观的工作方式。

外边距合并

当外边距意外合并,而你不知道发生了什么时,真的很令人困惑。当两到多个垂直相邻的外边距相接触,并且没有用内边距或者边框分隔开时,外边距有时会合并。如果子元素的外边距伸进其父元素的外边距,并且没有被内边距分隔开时,外边距合并也会发生。

如果元素是绝对定位、浮动或者有不同的格式化上下文以及在其它一些不可能的情况下时,外边距不会合并。

如果你感到困惑,没有关系。关于外边距什么时候会合并,什么时候不会合并的规则是很复杂的。你需要知道的主要事情是,当元素没有内边距或者边框时,垂直外边距可以合并。

如果你需要了解得更详细,CSS Tricks 上有一篇很精彩的外边距合并的解释文章

视觉格式化模型

盒模型计算元素的大小,而视觉格式化模型(Visual Formatting Model)负责确定这些盒子的布局。视觉可视化模型考虑盒子的类型、定位方案、元素之间的关系以及内容施加的约束,来确定页面上每个元素的最终位置和呈现。

你需要知道什么:

视觉格式化模型遍历文档树,根据 CSS 盒模型生成一到多个渲染元素所需的盒子。CSS 的 display 属性在确定一个元素如何参与当前上下文和定位方案中发挥关键作用。这些部分综合在一起,确定元素的最终布局和定位。

这是一个复杂的步骤,也是目前为止最难尝试和总结的。如果你还没了解所有这些知识的话,没有关系。理解如何通过 CSS 属性操纵定位方案和格式化上下文是个好的开端。如果你理解了该模型的不同部分之间的相互作用的话,你就会比大多数人做得更好。起码你应该知道它们的存在。

Display 类型

我们知道,在 CSS 中设置 display 属性可以确定如何渲染元素,但是还不清楚其工作原理。事实上,它有时甚至好像是不可预测的。

这是因为,display 属性确定了元素的”盒子类型“。这个隐藏属性由一个内部显示类型和一个外部显示类型组成,二者在一起帮助确定如何渲染元素。

外部显示类型通常要么是解析为 block,要么是解析为 inline,并且与我们对 CSS 中这些 display 属性的期望几乎是一致的。从技术上讲,外部显示类型规定一个元素如何参与其父元素的格式化上下文。

内部显示类型决定元素会生成什么样的格式化上下文。这会影响其子元素的布局排列方式。

想想弹性盒容器的工作机制。其外部类型是 block,内部类型是 flex。它的子元素也可以有一个 block 外部类型,不过子元素的布局是受弹性盒容器的格式化上下文所影响的。

思考这种问题的一个方法是,display 的职责是在元素及其父元素之间共享的。

格式化上下文

格式化上下文都是与布局有关。它们是控制容器内元素的布局以及它们之间如何相互作用的规则。

有些格式化上下文可以直接在容器上设置,比如通过用 display 属性值 flexgridtable。其它类型的格式化上下文,比如块格式化上下文和行内格式化上下文可以被浏览器根据需要创建。

注意:在过去,因为块格式化上下文与浮动交互的方式,理解如果让浏览器建立一个新的块格式化上下文是很重要的。设置为块格式化上下文的元素会包含浮动。不过现在它没有以前那么重要。事实上,它甚至都不是现代清除浮动技术的工作原理了。

定位方案

一个盒子可以根据三种定位方案之一来布局,包括常规流、浮动和绝对定位。你可能熟悉浮动和绝对定位,因为在编写 CSS 时,我们会更直接与这两个打交道。常规流只是一个在元素没有浮动或者定位时默认定位方案的名字。

常规流

常规流(Normal Flow)描述了默认定位方案,'in-flow'(流内)描述遵从该方案的元素。你可以把流内当作是元素根据其源顺序和格式化上下文布局时的自然位置。

浮动

浮动(float)是一个 CSS 属性,它会导致元素脱离常规流,尽可能远地向左或者向右移动,直到它挨着其包含盒或者另一个浮动元素的边缘。当浮动发生时,文本和行内元素会环绕着浮动元素。

正常情况下,如果没有设置浮动,一个元素的高度会调整以容纳其所有的后代元素。当元素被浮动时,就会从流中脱离出来,这意味着容器不会调整其高度以清除它们。

正是这种行为,让多行文本、标题以及其它元素环绕着浮动内容而流动。但是有时这是有问题的。清除浮动并设立新的块格式化上下文会导致容器清除其浮动子元素。这种技术允许浮动被用在布局上,已经成为 Web 开发技术的奠基石很久了。它依然是要知道的重要技术,不过正在逐渐被像 Flexbox 和 Grid 这种更新的布局技术所替代。

绝对定位

绝对定位的元素是彻底从流中删除,并且不像浮动元素,它们对周围的内容没有影响。

相对定位的容器允许我们用绝对定位来控制后代元素的偏移。

相对定位元素也可以设置偏移,不过这种偏移是相对于父元素的常规位置,而不是另一个相对定位的容器。

CSS 属性 topbottomleftright 被用于计算盒子的偏移。这些属性都不是二维偏移,不过都可以相对于其容器的内容盒来定位每个边。

具有重叠偏移的定位元素可以导致元素占用同一空间。堆叠上下文用于解决这个问题。

堆叠上下文

堆叠上下文决定渲染到页面上的东西的顺序。你可以把一个堆叠上下文当作一个层。堆叠底部的层先绘制,该堆叠之上更高的元素出现在上面。

在一个绝对或者相对定位元素上设置 z-index 属性是建立新的堆叠上下文的最常见手段。不过,有很多组成堆叠上下文的其它手段,包括设置透明度(opacity)、转换(transform)、滤镜(filter)或者使用 will-change 属性。

不过,使用这些其它手段的原因并不直观,并且比开发者的预期相比,这些手段对渲染性能有一定影响。只不过它们有助于理解这些层可以被浏览器单独渲染。因此,有时因为性能的原因,有意创建新的堆叠上下文是很有用的。

除非建立了堆叠上下文,否则设置 z-index 是没有效果的。z-index 越高,层在堆叠中放在的位置越高。

有关堆叠的最让人感到困惑的部分之一是,一个新的堆叠上下文可以建立在一个已有的堆叠上下文内。也就是说可以有层中层。

在这种情况下,并非总是最高的 z-index 会放在最上面。

就是这样了!

差不多 3000 个字,我只是大致介绍了 CSS 的重要隐藏部分的一些知识。如果你已经全部读完了,那么恭喜你,请一定让我知道,因为你值得被奖励!

如果你只是读了某些部分,也没关系。希望我已经设法澄清了一些事情,或者对涉及的过程给出了初步的见解。在不牺牲准确性的条件下,用简单的术语解释这些东西是一种真正的挑战。我希望我做对了。

本文链接:http://www.xiaojichao.com/post/the-invisible-parts-of-CSS.html

-- EOF --

Comments