【译】CSS 才不是什么黑魔法呢

CSS 才不是什么黑魔法呢

一块儿来揭开 CSS 的神秘面纱

若是你是一名 web 开发者,你可能会时不时地写一些 CSS。css

当你第一次接触 CSS 时,彷佛以为 CSS 垂手可得。加边框,改颜色,小菜一碟。JavaScript 才是前端开发的难点,不是吗?html

可是在你 web 开发生涯中的某天,这个想法变了!更糟糕的是,许多前端社区的开发者早已把 CSS 轻视为一门玩具语言。前端

然而,事实倒是当咱们碰壁时,咱们中的许多人实际上不曾深刻了解咱们编写的 CSS 作了什么。react

在我接受前端培训后的头两年,我曾从事全栈 JavaScript 开发,偶尔写一点点 CSS。做为 JavaScript Jabber 评委会的一员,我一直认为 JavaScript 才是我吃饭的家伙,因此大部分时间我都花在 JavaScript 上。android

然而直到去年,当我决定专一于前端时,才意识到根本没法像调试 JavaScript 那样轻松地调试 CSS!ios

咱们都喜欢拿 CSS 开玩笑,可是咱们中有多少人真的花时间去尝试理解咱们正在编写或正在阅读的 CSS。当咱们碰壁时,咱们有多少人在解决问题的同时,会深刻最底层(看看发生了什么)? 相反,咱们止步于照搬 StackOverflow 上票数最高的答案,或者用一些黑科技(hack)手段随便应付一下,或者咱们干脆撒手无论了:那是一个 feature 而不是一个 bug。git

当浏览器以非预期的方式呈现 CSS 时,开发者经常感到很是困惑。可是 CSS 并非黑魔法,而做为开发者,咱们都明白计算机只会按照咱们的指令去执行。github

学习浏览器的内部工做原理将有助于掌握高级调试技巧和性能优化方案。虽然许多会议的演讲会讨论如何修复常见的 bug,但个人演讲(和这篇文章)的重点在于为何会有这些 bug,为此我将深刻介绍浏览器内部原理,看看咱们的 CSS 是如何被解析和呈现。web

DOM 与 CSSOM

首先,了解浏览器包含 JavaScript 引擎和渲染引擎很是重要,而本文将重点关注后者。例如,咱们将讨论涉及 WebKit(Safari),Blink(Chrome),Gecko(Firefox)和 Trident / EdgeHTML(IE / Edge)的细节。浏览器将经历包括转换、标记化、词法分析和解析的过程,最终构建 DOM 和 CSSOM。(译注:CSSOM 即 CSS Object Model,定义了媒体查询,选择器和 CSS 自己的 API,这些 API 包括了通用解析和序列化规则,传送门:CSSOM后端

这一过程大体能够分为如下几个步骤:

  • 转换:从磁盘或网络读取 HTML 和 CSS 的原始字节。
  • 标记化: 将输入内容分解成一个个有效标记(例如:起始标签、结束标签、属性名、属性值),分离无关字符(如空格和换行符)。
  • 词法分析:和 tokenizer(标记生成器)相似,但它还标记每一个 token 的类型(类型包括:数字、字符串字面量、相等运算符等等)。
  • 解析: 解析器接收词法分析器传递的 tokens,并尝试将其与某条语法规则进行匹配,匹配成功后将之添加到抽象语法树中。

一旦 DOM 树和 CSSOM 树建立完毕,渲染引擎就会将数据结构附加到所谓的渲染树中,并做为布局过程的一部分。

渲染树是文档的可视化表现形式,它按照正确的顺序绘制页面的内容。渲染树的构造过程遵循如下顺序:

  • 从 DOM 树的根节点开始,遍历每一个可见节点
  • 忽略不可见的节点
  • 对于每一个可见节点,找到合适的与 CSSOM 匹配的规则并应用它们
  • 发送包含内容和计算样式的可见节点
  • 最后,在屏幕上输出包含全部可见元素的内容和样式信息的渲染树。

CSSOM 能够对渲染树产生很大的影响,但不会影响到 DOM 树。

渲染

经历了布局和渲染树构建后,浏览器终于要开始将网页绘制到屏幕上并合成图层。

  • 布局:包括计算一个元素占用的空间以及它在屏幕上的位置。父元素能够影响子元素布局,某些状况下子元素也会反过来影响父元素。
  • 绘制:将渲染树中的每一个节点转换为屏幕上的实际像素的过程。它涉及绘制文本、颜色、图像、边框和阴影。绘图一般在多个图层上完成,另外因为加载、执行 JavaScript 而改变了 DOM 会致使屡次绘制 。
  • 合成:将全部图层合并在一个图层,做为最终屏幕上可见图层的过程。因为页面的各个部分能够绘制成多层,因此须要以正确的顺序绘制到屏幕上。

绘制时间取决于渲染树结构,元素的 widthheight 的值越大,绘制时间就越长。

添加各类特效一样会增长绘画时间。绘制的顺序是按照元素进入层叠上下文的顺序(从后往前绘制),稍后咱们再谈谈 z-index。若是你喜欢看视频教程,有一个很棒的关于绘制过程的 demo

当人们在谈论浏览器的硬件加速时,绝大多数都是指加速“合成”过程,也就是意味着使用 GPU 来合成网页的内容。

与使用计算机 CPU 进行合成的旧方式相比,使用 GPU 能带来至关多的速度提高,而合理利用 will-change 这一属性有助于此。(译注:will-change 相关资料传送门 will-change MDNEverything You Need to Know About the CSS will-change Property

举个例子:在使用 CSS transform 属性时,will-change 属性能提早告知浏览器 DOM 元素接下来会有哪些变化。这能够将一些绘制和合成操做移交给 GPU,从而大大提升有大量动画的页面的性能。使用 will-change 属性,对于滚动位置变化、内容变化、不透明度变化以及绝对定位坐标位置变化也有相似的性能收益。

有必要了解一件事:某些 CSS 属性将致使从新布局,而其余属性只会致使从新绘制。固然出于性能考虑,最好只触发重绘。

举个例子:元素的颜色改变后,只会对该元素进行重绘。而元素的位置改变后,会对该元素及其子元素(可能还有同级元素)进行布局和重绘。添加 DOM 节点后,会对该节点进行布局和重绘。一些重大变化(例如增大 html 元素的字体)会致使整个渲染树进行从新布局和绘制。

若是你像我同样,比起 CSSOM 更熟悉 DOM,那么让咱们来深刻了解一下 CSSOM。请务必注意,默认状况下,CSS 会被视为阻塞渲染资源。这意味着浏览器在构建完 CSSOM 以前,将挂起任何其它进程的渲染。

CSSOM 和 DOM 并非一一对应的。具备 dispay:none 属性的元素、<script> 标签、<meta> 标签、<head> 元素等等不可见的 DOM 元素不会显示在渲染树中。

CSSOM 和 DOM 的另外一个区别则在于解析 CSS 使用的是一种上下文无关语法。也就是说,CSS 渲染引擎不会自动补全 CSS 中缺乏的语法,然而解析 HTML 建立 DOM 时则恰好相反。

解析 HTML 时,浏览器不得不结合 HTML 标签所在的上下文,并且只听从 HTML 规范是不够的,由于 HTML 标签可能包含一些缺省的信息,而且不管解析成什么,最终都要渲染出来。(译注:这么作的目的是为了包容开发者的错误,简化 web 开发,例如能省略一些起始或者结束标记等等)

说了那么多,咱们来回顾一下:

  • 浏览器向服务器发起 HTTP 请求
  • 服务器响应请求,并返回网页数据
  • 浏览器经过标记化将响应数据(字节)转换为 tokens
  • 浏览器将 tokens 转换为节点
  • 浏览器将节点插入 DOM 树
  • 等待构建 CSSOM 树

优先级

咱们已经深刻了解了很多浏览器的工做原理,那么接下来咱们来看看一些更常见的开发痛点吧。首先说说优先级。

简单来讲,CSS 的优先级是指以正确的层叠顺序应用规则。尽管可使用多种 CSS 选择器来选中特定的标签,浏览器仍须要一种方式来决定最终哪些样式将会生效。在决策过程当中,首先浏览器会计算每一个选择器的优先级。

不幸的是,优先级的计算规则难倒了很多 JavaScript 开发者,因此让咱们一块儿深刻研究 CSS 优先级的计算规则。咱们将使用如下的 html 结构做为例子:有一个类名为 container 的 div,在这个 div 里,咱们嵌套了另外一个 div,它的 id 是 main,咱们又在这个 div 里嵌套了一个包含 a 标签的 p 标签。别偷看答案,你知道 a 标签的颜色是什么吗?

#main a {
  color: green;
}

p a {
  color: yellow;
}

.container #main a {
  color: pink;
}

div #main p a {
  color: orange;
}

a {
  color: red;
}复制代码

(译注:加一段 html 结构顺便防偷看答案 →_→)

<div class="container">
    <div id="main">
        <p>
            <a href="#">Test</a>
        </p>
    </div>
</div>复制代码

答案是粉色,它的优先级为:1,1,1。如下是其他选择器的优先级:

  • div #main p a: 1,0,3
  • #main a: 1,0,1
  • p a: 2
  • a: 1

优先级的每个数的计算规则以下:

  • 第一个数:ID 选择器的数量
  • 第二个数:类选择器、属性选择器(不包含:[type="text"], [rel="nofollow"])、以及伪类选择器(不包含::hover, :visited)的数量和。
  • 第三个数:元素选择器与伪元素选择器(不包含: ::before, ::after)的数量和。

所以,对于如下选择器:

#header .navbar li a:visited复制代码

该选择器的优先级是:1,2,2。由于咱们有 1 个 ID 选择器、1 个类选择器、1 个伪类选择器、还有 2 个元素选择器(lia)。你能够把优先级看做一个数字,好比 1,2,2 就是 122。这里的逗号是为了提现你优先级的数值并非以 10 进制计算的。理论上你可让一个元素的优先级为:0,1,13,4,其中的 13 并不会像 10 进制那样产生进位。(译注:不会变成 0,2,3,4)

定位

其次,我想花点时间讨论一下定位。正如前文所说的,定位和布局是密切相关的。

布局是一个递归的过程,当全局样式变化的时候,有时会在整个渲染树上(从新)触发布局,有时则仅在局部变化的地方增量更新。有一件有趣的事情值得注意:若是咱们从新思考渲染树中的绝对定位元素,该对象在渲染树中的位置和它在 DOM 树中的位置不一样的。

我也常常被问及应该使用 flexbox 仍是 float 进行布局。毫无疑问,用 flexbox 进行布局至关方便,并且当应用于同一个元素时,flexbox 布局将在大约 3.5ms 内呈现,而 float 布局可能须要大约 14ms。因此,磨砺你的 CSS 技能所带来的回报不下于磨砺你的 JavaScript 技能的回报。

Z-Index

最后,我想聊聊 z-index。起初 z-index 听起来很简单。HTML 文档中的每一个元素均可以处在文档的每一个其余元素的前面或后面。 而它也只适用于指定了定位方式的元素(译注:即,未被定位,非 position:static 的元素)。若是你尝试在没有被定位的元素上设置 z-index,则不会起做用。

调试 z-index 问题的关键是理解层叠上下文,并始终从层叠上下文的根元素开始调试。 层叠上下文是 HTML 元素的三维概念,这些 HTML 元素在一条假想的相对于面向视窗(电脑屏幕)的用户的 z 轴上延伸。换句话说,它是一组具备相同父级的元素,在同一个层叠上下文领域,层叠水平值大的那一个覆盖小的那一个。

每一个层叠上下文都有一个惟一的 HTML 元素做为其根元素,而且在不涉及 z-indexposition 属性时,层叠规则很简单:层叠顺序与元素在 HTML 中出现的顺序相同。(译注:即,新绘制的元素会覆盖以前的元素)

固然,你也可使用 z-index 以外的属性来建立新的层叠上下文,这会致使状况更为复杂。如下属性都会建立新的层叠上下文:

  • opacity 值不是 1
  • filter 值不是 none
  • mix-blend-mode 值不是 normal

顺便提一下,blend mode 决定了指定图层上的像素与其下方图层上的可见像素的混合方式。

transform 属性值不为 none 的元素一样会建立新的层叠上下文。例如 scale(1)translate3d(0,0,0)。一样顺便提一下,scale 属性是用于调整元素大小的,而 translate3d 属性则会启用 GPU 加速让 CSS 动画更为流畅 。

因此,尽管你可能尚未设计师般的眼光,但但愿你正向着 CSS 大师迈进!若是你有兴趣了解更多,我整理了一些学习资源


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划

相关文章
相关标签/搜索