这是《From URL to Interactive》系列文章的第一篇《Server to Client》。《From URL to Interactive》是个引子就不译了,文章主要基于windows自带的浏览器Eage为基础介绍现代浏览器对HTML从请求、链接、加载、解析、渲染、交互的过程。分阶段介绍:
From URL to Interactive(一)---从服务器到客户端(Server to Client)
From URL to Interactive(二)---从标签到DOM(Tags to DOM)
From URL to Interactive(三)---从大括号到像素(Braces to Pixels)
From URL to Interactive(四)---从var到及时编译(Var to JIT)
篇原文地址:https://alistapart.com/article/braces-to-pixels
-----------------------------------------------------------------------------------------------------------------------------
CSS看起来不像魔术吗?好吧,在“ From URL to Interactive ”的第三部分中,我们将介绍浏览器将CSS从大括号转换为像素的过程。作为奖励,我们还将快速了解最终用户互动如何影响此过程。我们有很多理由可以覆盖,所以在这里拿一杯<插入你最喜欢的饮料的名字>,让我们开始吧。
解析
类似于我们在“ Tags to DOM ”中学习的HTML ,一旦浏览器下载了CSS,CSS解析器就会编制处理它遇到的任何CSS。这可以是单个文档中的CSS、<style>标记内部的,也可以是DOM元素style属性中内联的css。根据语法规范解析出所有CSS并进行标记化。在此过程结束时,会产生一个包含所有数据的数据结构,其中包含所有选择器,属性和属性的值。
例如,考虑以下CSS:
.fancy-button {
background: green;
border: 3px solid red;
font-size: 1em;
}
这将生成以下数据结构,以便在以后的过程中轻松使用:
有一两件事值得一提,浏览器爆炸性的生成background和border到他们的变体中,如速记主要用于提高开发人体工程学; 浏览器只处理来自这里的longhands。
完成此操作后,引擎继续构建DOM树,Travis Leithead在“ Tags to DOM ”中介绍了DOM树; 所以,如果你还没有读过,可以先看看
计算
现在我们已经在现有内容中解析了所有样式,现在是时候对它们进行样式计算了。我们尝试尽量对所有值减少到一个标准化的计算值。当计算阶段结束,任何尺寸值都减少到三种可能的输出:auto、percentage或pixel value。
现在我们已经计算处理了所有数据值并且放在存储中了,接下来就该处理级联了
级联
由于CSS可以来自各种来源,因此浏览器需要一种方法来确定哪些样式应该应用于给定的元素。为此,浏览器使用名为specificity的公式,该公式基于计算选择器中使用的标记,类,ID和属性选择器的数量,以及是!important声明出现的数量来决定。通过内联style属性在元素上定义的样式被赋予一个等级,该等级优先于<style>块或外部样式表中的任何样式。如果Web开发人员使用!important某个值,则该值将胜过任何CSS,无论其位置如何,除非还有!important内联。
为清楚起见,让我们看一下Web开发人员编写的一些示例以及计算后的结果:
那么,当特异性绑定(译者:计算好)后,引擎会做什么?对特异性相同的选择器,最后一个选择器会被选中。在以下示例中,div将具有蓝色背景。
div {
background: red;
}
div {
background: blue;
}
让我们.fancy-button稍微扩展一下我们的例子:
.fancy-button {
background: green;
border: 3px solid red;
font-size: 1em;
}
div .fancy-button {
background: yellow;
}
现在CSS将生成以下数据结构。我们将在整篇文章中继续这样做。
了解起源
在“ 服务器到客户端 ”中,Ali Alabbas讨论了与浏览器导航相关的“源”(origins)。在CSS中,也有源,但它们用于不同的目的:
- user:用户在用户代理(译者:通常都是浏览器)中设置的任何全局样式;
- author:网站开发者;
- user agent:任何可以利用和渲染CSS的东西(对大多数Web开发人员和用户来说,就是浏览器)。
这些起源中的每一个的级联能力确保最大的优先级是user,然后是author,最后是user agent。让我们进一步扩展我们的数据集,看看当用户将浏览器的字体大小设置为最小2em时会发生什么:
做级联
当浏览器具有来自所有来源的所有声明的完整数据结构时,它将根据规范对它们进行排序。首先,它将按原点排序,然后按特异性排序,最后按文档顺序排序。
这生成了.fancy-button “获胜”的属性和值(表中越高越好)。例如,从上表中,您会注意到用户的浏览器首选项设置优先于Web开发人员的样式。现在,浏览器找到与选择器匹配的所有DOM元素,并将得到的计算样式挂载到匹配的元素,在本例中div为.fancy-button:
如果您想了解有关级联如何工作的更多信息,请查看官方规范。
CSS对象模型
到目前虽然我们已经做了很多,但还没有完成所有工作。现在我们需要更新CSS对象模型(CSSOM)。CSSOM驻留在document.stylesheets中,我们需要更新它,以便它能将到目前为止已经解析和计算的所有内容展示出来。
Web开发人员甚至可能没有意识到这些信息。例如,在调用getComputedStyle()时,如果需要,上述过程也会运行。
布局(Layout)
现在我们已经应用了一个带有样式的DOM树,现在是时候开始构建一个用于视觉目的的树了。此树存在于所有现代引擎中,称为框树。为了构造这个树,我们遍历DOM树并创建零个或多个CSS框,每个框都有边距,边框,填充和内容框。
在本节中,我们将讨论以下CSS布局概念:
- 格式化上下文(FC):有许多类型的格式化上下文,其中大多数Web开发人员通过更改display元素的值来调用。一些最常见的格式化上下文是块(块格式化上下文或BFC),flex,grid,table-cells和inline。其他一些CSS也可以强制使用新的格式化上下文,例如position: absolute,float或使用multi-colum。
- 包含块:这是您解析样式的祖先(ancestor)块。
- 内联方向:这是文本布局的方向(译者:文本的方向),由元素的书写模式决定。在基于拉丁语的语言中,这是水平轴,在CJK语言中,这是垂直轴。
- 块方向:此行为与内联方向完全相同,但与内联轴垂直。因此,对于基于拉丁语的语言,这是垂直轴,而在CJK语言中,这是水平轴。
解决 AUTO
请记住,在计算阶段,维度值可以是三个值之一:auto,percentage或pixel。布局的目的是在框树中调整所有框的大小和位置,以便为绘画做好准备。作为一个非常直观的人,我发现例子可以让你更容易理解盒子树的构造方式。为了更容易理解,我不会显示单独的CSS框,只显示主框。让我们使用以下代码查看基本的“Hello world”布局:
<body>
<p>Hello world</p>
<style>
body { width: 50px; }
</style>
</body>
浏览器从body
元素开始。我们产出它的主要盒子,宽度为50px,默认高度为auto
。
现在移动到段落并生成其主框,并且由于段落默认具有边距(margin),这将影响正文的高度,如视觉中所反映的。
现在浏览器移动到“Hello world”文本,这是DOM中的文本节点。因此,我们生产线箱的布局里面。请注意,文本溢出了正文。我们将在下一步处理这个问题。
因为“world”不匹配而且我们没有将overflow
属性从其默认值更改,所以引擎会向其父级报告其在布局文本时left off的位置。
由于父级已收到其子级无法完成所有内容布局的令牌,因此它会克隆所有线框的样式,并传递该框的信息以完成布局。布局完成后,浏览器将返回框树,解析所有任何基于auto
或percentage的值。在上图中,您可以看到正文和段落现在包含所有“Hello world”,因为它height
被设置为auto
。
处理floats
现在让我们变得更复杂一些。我们将采用正常的布局,其中我们有一个“share it”的按钮,并将其float到拉丁文字段的左侧。float本身被认为是“shrink-to-fit”的背景。它被称为“shrink-to-fit”的原因是因为如果尺寸是这样的话,盒子将围绕其内容以auto值收缩。浮动框(Float box)是与此布局类型匹配的一种框,除此之外还有许多其他框,例如absolute positioned boxes(包括position: fixed元素)和具有基于auto大小的表格单元格(table cells)。
这是我们按钮场景的代码:
<article>
<button>SHARE IT</button>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pellentesq</p>
</article>
<style>
article {
min-width: 400px;
max-width: 800px;
background: rgb(191, 191, 191);
padding: 5px;
}
button {
float: left;
background: rgb(210, 32, 79);
padding: 3px 10px;
border: 2px solid black;
margin: 5px;
}
p {
margin: 0;
}
</style>
该过程与“Hello world”示例相同的模式开始,因此我将跳到我们开始处理浮动按钮的位置。
由于float创建了一个新的块格式化上下文(BFC)并且是一个shrink-to-fit context,因此浏览器会执行一种称为内容度量的特定类型的布局。在这种模式下,它看起来与其他布局相同,但有一个重要的区别,即它是在无限空间中完成的。浏览器在此阶段所做的工作是以最大和最小宽度布置BFC树。在这种情况下,它正在布置一个带有文本的按钮,因此它的最小尺寸(包括所有其他CSS框)将是最长字的大小。在最宽的一行,它将是一行中的所有文本,增加了CSS框。 注意:此处按钮的颜色不是文字的样式。它仅用于说明目的。
现在我们知道最小宽度是86px,最大宽度是115px,我们将这些信息传递回父框,以便它决定宽度并适当地放置按钮。在这种情况下,有足够的空间来容纳最大尺寸的浮动,这就是按钮布局的方式。
为了确保浏览器符合标准并且内容围绕float,浏览器会更改article
BFC 的几何结构。此几何体将传递给其在布局期间使用的段落(paragraph)。
从这里开始,浏览器遵循与第一个示例中相同的布局过程 - 但它确保任何inline内容的inline和块起始位置都在float所占用的约束空间之外。
当浏览器继续沿着树向下走并克隆节点时,它移动到约束空间的块位置外。这允许最后一行文本(以及它之前的那一行)在内联方向的内容框的开头处开始。然后浏览器返回树,并根据需要解析auto
或percentage值。
了解片段(UNDERSTANDING FRAGMENTATION)
关于布局如何工作的最后一个方面是片段化(fragmentation)。如果您曾经打印过网页或使用过CSS Multi-column,那么您已经利用了片段。片段化是将内容分开以使其适合不同几何形状的逻辑。让我们看一下使用CSS Multi-column的相同示例:
<body>
<div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras nibh orci, tincidunt eget enim et, pellentesque condimentum risus. Aenean sollicitudin risus velit, quis tempor leo malesuada vel. Donec consequat aliquet mauris. Vestibulum ante ipsum primis in faucibus </p>
</div>
<style>
body {
columns: 2;
column-fill: auto;
height: 300px;
}
</style>
</body>
一旦浏览器到达multicol格式化上下文框,就会看到它具有一定数量的列
它遵循之前类似的克隆模型,并创建一个具有正确尺寸的碎片器,以符合author对其列的期望。
浏览器按照与之前相同的模式,布置尽可能多的行。然后浏览器创建另一个片段并继续完成布局。
绘画(Painting)
好的,让我们回顾一下我们到目前为止所处的位置。我们已经取出所有CSS内容,解析它,将其级联到DOM树上,并完成布局。但是我们没有对布局应用颜色,边框,阴影和类似的设计处理 - 这些被称为绘画。
绘画大致由CSS标准化,简洁地说(你可以阅读CSS 2.2附录E中的完整细分),你按以下顺序绘画:
- 背景(background);
- 边界(border);
- 内容(content)。
因此,如果我们从之前的“SHARE IT”按钮开始并按照此过程进行操作,它将如下所示:
完成后,它将转换为位图(bitmap)。没错- 最终每个布局元素(甚至文本)都成为引擎盖下的图像。
关于 Z-INDEX
现在,我们的大多数网站都不包含单个元素。而且,我们经常希望某些元素出现在其他元素之上。为了实现这一点,我们可以利用z-index将一个元素叠加到另一个元素上。这可能与我们在设计软件中处理图层的方式很像,但是在浏览器的合成器中只存在的唯一图层。看起来好像我们正在创建新图层z-index,但我们不是 - 所以我们在做什么?
我们正在做的是创建一个新的堆叠上下文。创建新的堆叠上下文会有效地更改绘制元素的顺序。我们来看一个例子:
<body>
<div id="one"> Item 1 </div>
<div id="two"> Item 2 </div>
<style>
body {
background: lightgray;
}
div {
width: 300px;
height: 300px;
position: absolute;
background: white;
z-index: 2;
}
#two {
background: green;
z-index: 1;
}
</style>
</body>
如果没有z-index使用,上面的文档将按文档顺序绘制,这将“项目2”置于“项目1”的顶部。但是由于有了z-index,绘制顺序被更改。让我们像上面一样逐步完成每个阶段的布局。
浏览器以根框开头; 我们在后台画画。
然后,浏览器从文档顺序遍历到较低级别的堆叠上下文(在本例中为“项目2”)并开始按照上述相同的规则绘制该元素。
然后它遍历到下一个最高的堆叠上下文(在本例中为“Item 1”)并根据CSS 2.2中定义的顺序绘制它。
z-index与颜色无关,用户可以看到哪个元素,因此就可以看到对应的那些文本和颜色。
组成(COMPOSITION)
在这个阶段,我们至少有一个从绘画传递到合成器的位图。合成器的工作是创建一个或多个图层,并将位图渲染到屏幕以供最终用户查看。
这里有一个合理的问题是,“为什么任何网站都需要多个位图或合成器层?”嗯,从目前为止我们看过的例子来看,真的不需要。但让我们看一个更复杂的例子。让我们假设在一个假想的世界中,Office团队想要让Clippy重新上线,他们希望通过CSS变换来实现脉动效果。
动画Clippy的代码可能如下所示:
<div class="clippy"></div>
<style>
.clippy {
width: 100px;
height: 100px;
animation: pulse 1s infinite;
background: url(clippy.svg);
}
@keyframes pulse { from { transform: scale(1, 1); } to { transform: scale(2, 2); } }
</style>
当浏览器读取Web开发人员想要为Clippy动画设置无限循环时,它有两个选项:
- 它可以返回到动画的每一帧的重绘阶段,并生成一个新的位图以返回合成器。
- 或者它可以生成两个不同的位图,并允许合成器仅在应用了此动画的图层上执行动画。
在大多数情况下,浏览器将选择选项2并生成以下内容(我有意简化了Word Online为此示例生成的图层数量):
然后它将在正确的位置重新组合Clippy位图并处理脉动动画。这是一个很好的性能获胜,因为在许多引擎中,compositor都在自己的线程上,这样就可以解除主线程的阻塞。如果浏览器选择上面的选项1,则必须在每个帧上阻塞以实现相同的结果,这将对最终用户的性能和响应性产生负面影响。
创造互动的错觉
正如我们刚刚学到的,我们将所有样式、DOM和生成的图像渲染给终端用户。那浏览器如何创建交互呢?Welp,我相信你现在已经知道了,所以让我们用 “SHARE IT”按钮作为类比的例子来看下:
button { float: left; background: rgb(210, 32, 79); padding: 3px 10px; border: 2px solid black; } button:hover { background: teal; color: black; }
我们在这里添加的是一个伪类,告诉浏览器在用户将鼠标悬停在按钮上时更改按钮的背景和文本颜色。这引出了一个问题,浏览器如何处理?
浏览器不断跟踪各种输入,当这些输入正在移动时,它会经历称为hit testing的过程。对于此示例,该过程如下所示:
- 用户将鼠标移到按钮上。
- 浏览器触发鼠标移动的事件并进入命中测试算法,该算法基本上问了一个问题:“鼠标触摸的是什么框?”
- 该算法返回链接到我们的“SHARE IT”按钮的框。
- 浏览器问一个问题,“鼠标悬停在你身上,我有什么要做吗?”
- 它快速运行此框及其子框的样式/级联,并确定,是的,:hover在声明块内部有一个仅使用绘制样式调整的伪类。
- 它将这些样式挂起DOM元素(正如我们在级联阶段所学到的),button在这种情况下就是这样。
- 它跳过布局并直接绘制新的位图。
- 新位图将传递给合成器,然后传递给用户。
对于用户来说,这有效地创建了交互感,即使浏览器只是将橙色图像交换为绿色图像。
Etvoilà!
希望这已经消除了从你编写CSS的大括号开始到浏览器中如何渲染像素的一些神秘感。
在我们这段旅程中,我们讨论了如何解析CSS,如何计算值以及级联实际如何工作。然后我们讨论了布局,绘画和构图。
现在请继续关注本系列的最后一部分,其中JavaScript语言的设计者之一将讨论浏览器如何编译和执行我们的JavaScript。
关于作者
Greg Whitworth
Greg致力于Microsoft Edge的渲染引擎,并且是丰富Web平台以增强Web开发人员能力的狂热倡导者。他是W3C CSS工作组和CSS Houdini工作组的成员。加入Microsoft前,他是一名有十多年全职从事中小型站点和Web应用程序开发经验的Web开发人员,。