微信公众号:爱写bugger的阿拉斯加
如有问题或建议,请后台留言,我会尽力解决你的问题。
前言
此文章是我最近在看的【WebKit 技术内幕】一书的一些理解和做的笔记。
而【WebKit 技术内幕】是基于 WebKit 的 Chromium 项目的讲解。
书接上文 浏览器内核之 HTML 解释器和 DOM 模型
本文剖析 WebKit 的 CSS 解释器和样式布局。
从整个网页的加载和渲染过程来看,CSS 解释器和规则匹配处于 DOM 树建立之后,RenderObject 树建立之前,CSS 解释器解释后的结果会保存起来,然后 RenderObject 树基于该结果来进行规范匹配和布局计算。当网页有用户交互或者动画等动作的时候,通过 CSSDOM 等技术,JavaScript 代码同样可以非常方便地修改 CSS 代码,WebKit 此时需要重新解释样式并重复以上这一过程。
1. CSS 基本功能
1.1.1 简介
CSS 的全称是 Cascading Style Sheet,中文名是级联样式表,主要用来控制网页的显示风格。
1.1.2 样式规则
图 6-1 描述了一个典型的 CSS 规则结构。一个规则包括两个部分——规则关和规则体。规则头由一个或者多个选择器组成;规则体则由一个或者多个样式声明组成,每个样式声明由样式名和样式值构成,表示这个规则对哪些样式进行了规定和设置。
当 HTML 中的某个元素经过后面的匹配算法使用了这条规则,那么将这些样式设置成该元素的样式,除非有更高优先级的规则匹配上该元素。
1.1.3 选择器
CSS 的选择器是一级模式,用来匹配相应的 HTML 元素。当选择器匹配相应元素的时候,该选择器包含的各种样式值就会作用于匹配的元素上。通过选择器,CSS 能够精准地控制 HTML 页面中的任意一个或者多个元素的样式属性。
具体的,这里不做介绍,请查阅 CSS 规范。
1.1.4 框模型
框模型(Box model,或称箱子模型)就是我们常说的盒子模型,是CSS 标准中引入来表示 HTML 标签元素的布局结构。一个框模型大致包括四个部分:外边距(Margin)、边框(Border)、内边距(Padding)和内容(Content)。
1.4.5 包含块(Containing Block)模型
当 WebKit 计算元素的箱子的位置和大小时,WebKit 需要计算该元素和另外一个矩形区域的相对位置,这个矩形区域称为该元素的包含块。上面介绍的框模型就是在包含块内计算和确定各个元素的,包含块的具体定义如下:
- 根元素的包含块称为初始包含块,通常它的大小就是可视区域(Viewport)的大小。
- 对于其他位置属性设置为 “static” 或者 “relative” 的元素,它的包含块就是最近祖先的箱子模型中的内容区域(Content)。
- 如果元素的位置属性为 “fixed” ,那么该元素的包含快脱离 HTML 文档,因定在可视区域的某个特定位置。
- 如果元素的位置属性为 “absolute” ,那么该元素的包含块由最近的含有属性 “absolute”、“relative”、或者 “fixed” 的祖先决定,具体规则如下:如果一个元素具有 “inline” 属性,那么元素的包含块是该祖先的第一个和最近一个 inline 框的内边距的区域;否则,包含块则是该祖先的内边距所包围的区域。
1.1.6 CSS 样式属性
CSS 标准中定义了各式各样的样式属性,用来描述元素的显示效果。
这些属性大致分成以下类型:
- 背景:如背景颜色和背景图片等。
- 文本:设置文本缩进,对齐。单词间隔。字母间隔。字符转换、装饰和空白字符等。
- 字体:设置字体属性,可以是内嵌的,也可以是自定义字体的方式,另外还可以设置加粗、变形等属性。
- 列表:设置列表类型,可以以字母、希腊字母、数字等方式编号列表。
- 表格:通过设置边框来达到显示表格的视觉效果的目的。设置是否把表格边框合并为单一的边框,设置分隔单元格边框的距离,设置表格标题的位置,设置是否显示表格中的空单元格,设置显示单元、行和列的算法等。
- 定位:CSS 提供元素的相对、绝对定位和浮动定位。
1.1.7 CSSOM(CSS Object Model)
CSSOM 称为 CSS 对象模型。它思想是在 DOM 中的一些节点接口中,加入获取和操作 CSS 属性或者接口的 JavaScript 接口,因而 JavaScript 可以动态操作 CSS 样式。DOM 提供了接口让 JavaScript 修改 HTML 文档,同理,CSSOM 提供了接口让 JavaScript 获得和修改 CSS 代码设置的样式信息。
对于内部和外部样式表,CSSOM 定义了样式表的接口,称为 “CSSStyleSheet”, 这是一个可以在 JavaScript 代码中访问的接口。借助这个接口,开发者可以在 JavaScript 中获取样式表的各种信息,例如 CSS 的 “href”、样式表类型 “type”、规则信息 “cssRules” 等,甚至可以获取样式表中的 CSS 规则列表。这个接口同 DOM 中的 “Script” 节点或者 “Link” 节点不一样,它是 CSSOM 定义的新接口。开发者可以通过 document.styleSheets 查看当前网页中包含的所有 CSS 样式表,这是因为 CSSOM 对 DOM 中的 Document 接口进行了扩展,下面是新加入的属性:
W3C 还定义了另外一个规范是 CSSDOM View,它的基本含义是增加一些新的属性到 Window、Document、Element、HTMLElement 和 MouseEvent 等接口,这些 CSS 的属性能够让 JavaScript 获取视图信息,用于表示跟视图相关的特征,例如窗口大小,网页滚动位移,元素的框位置、鼠标事件的坐标等信息。下面是以 CSSDOM View 对 Window 的扩展:
1.2 CSS 解释器和规则匹配
1.2.1 样式的 WebKit 表示类
对于 CSS 样式表,不管是内嵌还是外部文档,WebKit 都使用 CSSStyleSheet 类来表示。图 6-5 描述了 WebKit 内部是如何表示 CSS 文档的。
一切的起源都是从 DOM 的 Document 类开始。
- DoucmentStyleSheetCollection 类,该类包含了所在 CSS 样式表
- WebKit的内部表示类 CSSStyleSheet,它包含 CSS 的 href 、类型、内容等信息。
- CSS 的内容就是样式信息 StyleSheetContent,包含了一个样式规则 (StyleRuleBase)列表。样式规则被 用在 CSS 的解释器的工作过程中。
下面部分 WebKit 主要是将解释之后的规则组织起来,用于为 DOM 中的元素匹配相应的规则,从而应用规则中的属性值序列。这一过程的主要负责者是 StyleSheetResolver 类,它属于 Document 类,并包含了一个 DocumentRuleSets 类用来表示多个规则集合(RuleSet)。每个规则集合就是将之前解释之后的结果合并起来,并进行分类,例如 id 类规则,标签类规则等。至于为什么是多个规则集合,是因为这些规则集合可能源自于默认的规则集合,或者网页自定义的规则集合等。
1.2.2 解释过程
CSS 解释过程是指从 CSS 字符串经过 CSS 解释器处理后变成渲染引擎内部规则的表示过程。
在 WebKit 中,过程如 6-8 所示。
这一过程是基本思想是由 CSSParser 类负责。CSSParser 类其实也是桥接类,实际的解释工作是由 CSSGrammer.y.in 来完成。CSSGrammer.y.in 是Bison 的输入文件,Bioson 是一个生成解释器的工具。Bison 根据 CSSGrammer.y.in 生成 CSS 解释器——CSSGrammer 类。当然 CSSGrammer 类需要调用 CSSParser类来处理解释结果,例如需要使用 CSSParser 类创建选择器对象、属性、规则等。
在解释网页中自定义的 CSS 样式之前,实际上 WebKit 渲染引擎会为每个网页设置一个默认的样式,这决定了网页所没有设置的元素属性及其属性默认值和将要显示的效果。一般来讲,不同的 WebKit 移植可以设置不同的默认样式。下面是 Chrome 浏览器使用的默认样式,这些样式决定了默认的网页显示效果。
1.2.4 样式规则匹配
样式规则建立完成之后,WebKit 保存规则结果在 DocumentRuleSets 对象类中。当 DOM 的节点建立之后,WebKit 会为其中的一些节点(只限于可视节点)选择合适的样式信息。这些工作都是由 StyleResolver 来负责。当然,实际的匹配工作还是在 DocumentRuleSets 类中完成的。
图 6-9 描述了参与样式规则匹配的 WebKit 主要相关类。基本思路是使用 StyleResolver 类来为 DOM 的元素节点匹配样式。StyleResolver 类根据元素的信息,例如标签名、类别等,从样式规则中查找最匹配的规则,然后将样式信息保存到新建的 RenderStyle 对象中。最后,最后这些 RenderStyle 对象被 RenderObject 类所管理和使用。
规则的匹配则是由 ElementRuleCollector 类来计算并获得,它根据元素的属性等,并从 DocumentRuleSets 类中获取规则集合,依次按照 ID、类别、标签等选择器信息逐次匹配获得元素的样式。
首先,当 WebKit 需要为 HTML 元素创建 RenderObject 类的时候,首先 StyleResolver 类负责获取样式信息,并返回 RenderStyle 对象,RenderStyle 对象包含了匹配完的结果样式结果。
其次,根据实际需求,每个元素可能需要匹配不同来源的规则,依次是用户代理(浏览器)规则集合、用户规则集合和 HTML 网页中包含的自定义规则集合。这三个规则的匹配方式是类似的。这里是以自定义规则匹配为例加以说明的。
再次,对于自定义规则集合,它先查找 ID 规则,检查有无匹配的规则,之后依次检查类型规则,标签规则等,如果某个规则匹配上该元素,WebKit 把这些规则保存到匹配结果中。
最后,WebKit 对这些规则进行排序。对于该元素需要的样式属性,WebKit 选择从高优先级规则中选取,并将样式属性值返回。
1.2.5 JavaScript 设置样式
CSSDOM 定义了 JavaScript 访问样式的能力和方式。使用 CSSDOM 接口来更改属性值的过程,在 WebKit 中,这需要 JavaScript 引擎和渲染引擎协同完成。
大致的过程是,JavaScript 引擎调用设置属性值的公共处理函数,然后该函数调用属性值解析函数,在这个例子中则是 CSS 的 JavaScript 绑定函数。而后 WebKit 将解释后的信息设置到元素的 “style” 属性的样式 “webkitTransform” 中,然后设置标记表明该元素需要重新计算样式,并触发重新计算布局。最后是 WebKit 的重新绘图,图 6-12 描述了其中的主要过程。
1.3 WebKit 布局
1.3.1 基础
当 WebKit 创建 RenderObject 对象之后,每个对象是不知道自己的位置、大小等信息的,WebKit 根据框模型来计算它们的位置,大小等信息的过程称为布局计算。
第五章描述过 Frame 类,用于表示网页的框结构,每个框都有一个 FrameView 类,用于表示框的视图结构。
FrameView 类主要负责视图方面的任务,例如网页视图大小,滚动、布局计算、绘图等,它是一个总入口类。 “layout” 和 “needsLayout” ,它们用来布局计算和决定是否需要布局计算,实际的布局计算则是在 RenderObject 类中。
布局计算根据其计算的范围大致可以分为两类:第一类是对整个 RenderObject 树进行的计算;第二类是对 RenderObject 树中某个子树的计算,常见于文本元素或者是 overflow:auto 块的计算,这种情况一般是其子树布局的改变不会影响其周围元素的布局,因而不需要重新计算更大范围内的布局。
1.3.2 布局计算
布局计算是一个递归的过程,因为一个节点的大小通常需要先计算它的子女节点的位置,大小等信息。
图 6-14 描述了 RenderObject 节点计算布局的主要过程,中间省略了很多判断和步骤,主要逻辑都是由 RenderObject 类的 “layout” 函数来完成。
首先,该函数会判断 RenderObject 节点是否需要重新计算,通常这需要通过检查位数组中的相应标记位、子女是否需要计算布局等来确定。
其次,该函数会确定网页的宽度和垂直方向上的外边距,这是因为网页通常是垂直方向上滚动,而水平方向尽量不需要滚动。
再次,该函数会遍历其每一个子女节点,依次计算它们的布局。每一个元素会实现自己的 “layout” 函数,根据特定的算法来计算该类型元素的布局。如果页面元素定义了自身的宽高,那么 WebKit 按照定义的宽高来确定元素的大小,而对于像文本节点这样的内联元素则需要结合其字号大小及文字的多少等来确定其对应的宽高。如果页面元素所确定的宽高超过了布局容器包含块所能提供的宽高,同时其 overflow 的属性为 visible 或 auto , WebKit 则会提供滚动条来保证可以显示其所有内容。除非网页定义了页面元素的宽高,一般来说页面元素的宽高是在布局的时候通过相关计算得出来的。如果元素它有子女,则 WebKit 需要递归这一过程。
最后,节点根据它的子女们的大小计算得出自己的高度,整个过程结束。
重新布局的情况:
首先,当网页首次被打开的时候,浏览器设置网页的可视区域(viewport),并调用计算布局的方法。这其实也描述了一种常见的情景,就是当可视区域发生变化的时候,WebKit 都需要重新计算布局,这是因为网页的包含块的大小发生了改变。
其次,网页的动画会触发布局计算。当网页显示结束后,动画可能改变样式属性,那么 WebKit 就需要重新计算。
然后,JavaScript 代码通过 CSSDOM 等直接修改样式信息,它们也会触发 WebKit 重新计算布局。
最后,用户的交互也会触发布局计算,例如翻滚网页,这会角触发新区域布局计算。
CSS 的布局计算是以包含块和框模型为基础的,这表示这些元素的布局计算都依赖于块,例如 “div” 通常就是一个块,如前面所述它们通常是在垂直方向上展开。
但是,CSS 标准也规定了行布局形式,这就是内联元素。内联元素表现的是行布局形式,就是说这些元素以行进行显示。
以 “div” 元素为例,如果设置属性 “style” 为 “displa: inline” 时,则该元素是内联元素,那么它可能与前面的元素在同一行。如果该元素没有设置这个属性时,则是块元素,那么在新的行里显示。这显然会增加处理的复杂性,为此,WebKit 的处理方式是 ——对于一个块元素对应的 RenderObject 对象,它的子女要么都是块元素的 RenderObject 对象,要么都是非内联元素对应的 RenderObject 对象,这可以通过建立匿名块(Anonymous Block)对象来实现。
布局计算相对也是比较耗时间的,更糟糕的是,一旦布局发生变化,WebKit 就需要后面的重新绘制操作。另一方面,减少样式的变动而依赖现在 HTML5 的新功能可以有效地提高网页的渲染效率。
总结
- 匹配算法结合 CSS 规则来设置样式
- 选择器就是选中某个元素的
- 框模型就是常说的盒子模型,包含 margin、border、padding、content
- CSSOM 称为 CSS 对象模型,JavaScript 可以获取和操作 CSS 属性。
- CSS 解释过程是指从 CSS 字符串经过 CSS 解释器处理后变成渲染引擎内部规则的表示过程。
- 当 WebKit 创建 RenderObject 对象之后,每个对象是不知道自己的位置、大小等信息的,WebKit 根据框模型(Frame 类的 FrameView)来计算它们的位置,大小等信息的过程称为布局计算
- 布局计算是一个递归的过程,而且还会发生重新布局。
最后
希望本文对你有点帮助。
下期分享 第七章 渲染基础 敬请期待。
对 全栈开发 有兴趣的朋友可以扫下方二维码关注我的公众号 —— 爱写bugger的阿拉斯加
分享 web 开发相关的技术文章,热点资源,全栈程序员的成长之路。