这篇文章是以色列开发人员塔利·加希尔的研究成果。她在查阅了所有公开发布的关于浏览器内部机制的数据,并花了很多时间来研读网络浏览器的源代码。她写道:
本篇文章的英文原版:How Browsers Work: Behind the Scenes of Modern Web Browsers。
第一章 简介浏览器是使用最广的软件之一。在这篇博文中,我将介绍浏览器的幕后工作原理。通过阅读本文,我们将会了解,从您在地址栏输入 google.com ,直到您在浏览器屏幕上看到 Google 首页的整个过程中都发生了些什么。 1.1 讨论的浏览器目前使用的主流浏览器有五个:Internet Explorer、Firefox、Safari、Chrome和 Opera浏览器。本文主要以开源浏览器为主进行分析,即 Firefox、Chrome和 Safari(部分开源)。根据 StatCounter 浏览器统计数据,目前(2016年 2 月)Firefox(14.67%)、Safari(9.46%)和 Chrome(55.33%) 浏览器的总市场占有率将近 80%(这个数字在2011年8月的时候,才将近60%)。由此可见,如今开源浏览器在浏览器市场中占据绝大多数的市场份额。 1.2 浏览器的主要功能浏览器的主要功能就是向服务器发出请求,在浏览器窗口中展示您想要访问的网络资源。这里所说的资源一般是指 HTML 文档,也可以是 PDF、图片或其他的类型。资源的位置由用户使用 URI(统一资源标示符)指定。 浏览器解释并显示HTML文件的方式是在 HTML 和 CSS 规范中指定的。这些规范由网络标准化组织 W3C(万维网联盟)进行维护。多年以来,各浏览器都没有完全遵从这些规范,同时还在开发自己独有的扩展程序,这给网络开发人员带来了严重的兼容性问题。如今,大多数的浏览器都开始尽量遵从规范。 浏览器的用户界面有很多彼此相同的元素,其中包括:用来输入 URI 的地址栏;前进和后退按钮;书签设置选项;用于刷新和停止加载当前文档的刷新和停止按钮;用于返回主页的主页按钮。 1.3 浏览器的高层结构(High Level Structure)浏览器的主要组件包括:
第二章 渲染引擎(The rendering engine)渲染引擎的职责就是渲染,即在浏览器窗口中显示所请求的内容。这是每一个浏览器的核心部分,所以渲染引擎也称为浏览器内核。 默认情况下,渲染引擎可显示 HTML 和 XML 文档及图片。通过插件(或浏览器扩展程序),还浏览器渲染引擎也可以显示其它类型的内容。例如,使用 PDF 查看器插件就能显示 PDF 文档。在本章中,我们将集中介绍其主要用途:显示应用了CSS的 HTML 内容和图片。 2.1 渲染引擎简介本文所讨论的浏览器(Firefox、Chrome和Safari)是基于两种渲染引擎构建的。Firefox 使用的是 Gecko,这是 Mozilla 公司“自制”的渲染引擎。而 Safari 和 Chrome(28版本以前)浏览器使用的都是 Webkit。
Webkit 是一种开放源代码渲染引擎,起初用于 Linux 平台,随后由 Apple 公司进行修改,从而支持苹果机和 Windows。有关详情,请参阅 。 2.2 主流程(The main flow) 渲染引擎一开始会从网络层获取请求文档的内容,通常以8K分块的方式完成。
渲染引擎解析HTML文档,并将文档中的标签转化为dom节点树,即”内容树”。同时,它也会解析外部CSS文件以及style标签中的样式数据。这些样式信息连同HTML中的”可见内容”一道,被用于构建另一棵树——”渲染树(Render树)”。 渲染树由一些带有视觉属性(如颜色、大小等)的矩形组成,这些矩形将按照正确的顺序显示在频幕上。 渲染树构建完毕之后,将会进入”布局”处理阶段,即为每一个节点分配一个屏幕坐标。再下一步就是绘制(painting),即遍历render树,并使用UI后端层绘制每个节点。
主流程示例
从图2.2 和图2.3可以看出,虽然 Webkit 和 Gecko 使用的术语略有不同,但整体流程还是基本相同的。
第三章 解析与DOM树构建(Parsing and DOM tree construction)3.1 解析(Parsing-general) 既然解析是渲染引擎中一个非常重要的过程,我们将稍微深入的研究它。首先明白什么叫做解析(parsing)。 例如, 解析“2+3-1”这个表达式,可能返回这样一棵树。
1.文法(Grammars)解析是以文档所遵循的语法规则(编写文档所用的语言或格式)为基础的。所有可以解析的格式都必须对应确定的语法(由词汇和语法规则构成)。这称为与上下文无关的文法。人类语言并不属于这样的语言,因此无法用常规的解析技术进行解析。 2.解析器-词法分析器(Parser-Lexer combination) 解析一般可分为两个子过程:语法分析和词法分析。 解析工作一般由两个组件共同完成:
解析是一个迭代的过程。通常,解析器会向词法分析器请求一个新标记,并尝试将其与某条语法规则进行匹配。如果发现了匹配规则,解析器会将一个对应于该标记的节点添加到解析树中,然后继续请求下一个标记。 3.转换(Translation)很多时候,解析树还不是最终结果。解析通常是在转换过程中使用的,而转换是指将输入文档转换成另一种格式。编译就是一个例子。编译器可将源代码编译成机器代码,具体过程是首先将源代码解析成解析树,然后将解析树翻译成机器代码文档。
4.解析示例(Parsing example)在图3.1中,我们通过一个数学表达式建立了解析树。现在,让我们试着定义一个简单的数学语言,用来演示解析的过程。 词汇表:我们用的语言可包含整数、加号和减号。 5.词汇和语法的正式定义
正如您所看到的,这里用正则表达式给出了整数的定义。
之前我们说过,如果语言的语法是与上下文无关的语法,就可以由常规解析器进行解析。
6.解析器类型有两种基本类型的解析器:自上而下解析器和自下而上解析器。直观地来说,自上而下的解析器从语法的高层结构出发,尝试从中找到匹配的结构。而自下而上的解析器从低层规则出发,将输入内容逐步转化为语法规则,直至满足高层规则。 让我们来看看这两种解析器会如何解析我们的示例: 自下而上的解析器将扫描输入内容,找到匹配的规则后,将匹配的输入内容替换成规则。如此继续替换,直到输入内容的结尾。部分匹配的表达式保存在解析器的堆栈中。
这种自下而上的解析器称为移位归约解析器,因为输入在向右移位(设想有一个指针从输入内容的开头移动到结尾),并逐渐简化语法规则。 7.自动生成解析器(Generating parsers automatically) 解析器生成器这个工具可以自动生成解析器,只需要指定语言的文法———词汇表及语法规则,它就可以生成一个解析器。创建一个解析器需要对解析有深入的理解,而且手动的创建一个有较好性能的解析器并不容易,所以解析生成器很有用。 3.2 HTML 解析器(HTML Parser)HTML 解析器的任务是将 HTML 标记解析成解析树。 1.HTML 语法定义(The HTML grammar definition)W3C组织制定规范定义了HTML的词汇表和语法。 2.非与上下文无关的语法(Not a context free grammar) 正如在解析简介中提到的,上下文无关文法的语法可以用类似BNF的格式来定义。 这初看起来很奇怪:HTML 和 XML 非常相似。有很多 XML 解析器可以使用。HTML 存在一个 XML 变体 (XHTML),那么有什么大的区别呢?区别在于 HTML 的处理更为“宽容”,它允许您省略某些隐式添加的标记,有时还能省略一些起始或者结束标记等等。和 XML 严格的语法不同,HTML 整体来看是一种“软性”的语法。 显然,这种看上去细微的差别实际上却带来了巨大的影响。一方面,这是 HTML 如此流行的原因:它能包容您的错误,简化网络开发。另一方面,这使得它很难编写正式的语法。概括地说,HTML 无法很容易地通过常规解析器解析(因为它的语法不是与上下文无关的语法),也无法通过 XML 解析器来解析。 3.HTML DTD HTML的定义采用了DTD格式。此格式适用于定义SGML族的语言。它包括所有允许使用的元素及其属性和层次结构的定义。如上文所述,HTML DTD无法构成与上下文无关的语法。 4.DOM 解析器的输出(即”解析树”)是由DOM元素及属性节点组成的。DOM是文档对象模型(Document Object Model) 的缩写。它是HTML文档的对象表示,同时也是外部内容(例如 JavaScript)与HTML元素之间的接口。
可翻译成如下的 DOM 树:
和HTML一样,DOM也是由W3C组织制定的。请参见www.w3.org/DOM/DOMTR。这是关于文档操作的通用规范。其中一个特定模块描述针对HTML的元素。HTML的定义可以在这里找到:www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html。
5.解析算法(The parsing algorithm) 我们在之前章节已经说过,HTML无法用常规的自上而下或自下而上的解析器进行解析。原因在于: 符号化是词法分析的过程,将输入内容解析成多个标记,HTML标记包括起始标记、结束标记、属性名称和属性值。标记生成器识别标记,传递给树构造器,然后读取下一个字符以识别下一个标记,如此反复直到输入的结束。
6.符号识别算法(The tokenization algorithm)该算法的输出结果是HTML标记。该算法使用状态机来表示。每一个状态接收来自输入信息流的一个或多个字符,并根据这些字符更新下一个状态。当前的标记化状态和树结构状态会影响进入下一状态的决定。这意味着,即使接收的字符相同,对于下一个正确的状态也会产生不同的结果,具体取决于当前的状态。该算法相当复杂,无法在此详述,所以我们通过一个简单的示例来帮助大家理解其原理。 基本示例 - 将下面的 HTML 代码标记化:
初始状态是数据状态。遇到字符 遇到 现在我们回到“标记打开状态”。接收下一个输入字符
7.树构建算法在创建解析器的同时,也会创建 Document 对象。在树构建阶段,以 Document 为根节点的 DOM 树也会不断进行修改,向其中添加各种元素。标记生成器发送的每个节点都会由树构建器进行处理。规范中定义了每个标记所对应的 DOM 元素,这些元素会在接收到相应的标记时创建。这些元素不仅会添加到 DOM 树中,还会添加到开放元素的堆栈中。此堆栈用于纠正嵌套错误和处理未关闭的标记。其算法也可以用状态机来描述。这些状态称为“插入模式”。 让我们来看看示例输入的树构建过程:
树构建阶段的输入是一个来自标记化阶段的标记序列。第一个模式是“initial mode”。接收 HTML 标记后转为“before html”模式,并在这个模式下重新处理此标记。这样会创建一个 HTMLHtmlElement 元素,并将其附加到 Document 根对象上。 然后状态将改为“before head”。此时我们接收“body”标记。即使我们的示例中没有“head”标记,系统也会隐式创建一个 HTMLHeadElement,并将其添加到树中。 现在我们进入了“in head”模式,然后转入“after head”模式。系统对 body 标记进行重新处理,创建并插入 HTMLBodyElement,同时模式转变为“body”。 现在,接收由“Hello world”字符串生成的一系列字符标记。接收第一个字符时会创建并插入“Text”节点,而其他字符也将附加到该节点。 接收 body 结束标记会触发“after body”模式。现在我们将接收 HTML 结束标记,然后进入“after after body”模式。接收到文件结束标记后,解析过程就此结束。 ![]()
8.解析结束后的操作在此阶段,浏览器会将文档标注为交互状态,并开始解析那些处于“deferred”模式的脚本,也就是那些应在文档解析完成后才执行的脚本。然后,文档状态将设置为“完成”,一个“加载”事件将随之触发。您可以在 HTML5 规范中查看标记化和树构建的完整算法 9.浏览器的容错机制您在浏览 HTML 网页时从来不会看到“语法无效”的错误。这是因为浏览器会纠正任何无效内容,然后继续工作。 以下面的 HTML 代码为例:
在这里,我已经违反了很多语法规则(“mytag”不是标准的标记,“p”和“div”元素之间的嵌套有误等等),但是浏览器仍然会正确地显示这些内容,并且毫无怨言。因为有大量的解析器代码会纠正 HTML 网页作者的错误。 不同浏览器的错误处理机制相当一致,但令人称奇的是,这种机制并不是 HTML 当前规范的一部分。和书签管理以及前进/后退按钮一样,它也是浏览器在多年发展中的产物。很多网站都普遍存在着一些已知的无效 HTML 结构,每一种浏览器都会尝试通过和其他浏览器一样的方式来修复这些无效结构。 HTML5 规范定义了一部分这样的要求。Webkit 在 HTML 解析器类的开头注释中对此做了很好的概括。 解析器对标记化输入内容进行解析,以构建文档树。如果文档的格式正确,就直接进行解析。 遗憾的是,我们不得不处理很多格式错误的 HTML 文档,所以解析器必须具备一定的容错性。 我们至少要能够处理以下错误情况: 明显不能在某些外部标记中添加的元素。在此情况下,我们应该关闭所有标记,直到出现禁止添加的元素,然后再加入该元素。 使用了
请注意,错误处理是在内部进行的,用户并不会看到这个过程。
Webkit 会将其层次结构更改为两个同级表格:
代码如下:
Webkit 使用一个堆栈来保存当前的元素内容,它会从外部表格的堆栈中弹出内部表格。现在,这两个表格就变成了同级关系。
过于复杂的标记层次结构 示例网站 www.liceo.edu.mx 嵌套了约 1500 个标记,全都来自一堆
放错位置的 html 或者 body 结束标记。 同样,代码的注释已经说得很清楚了。支持格式非常糟糕的 HTML 代码。我们从不关闭 body 标记,因为一些愚蠢的网页会在实际文档结束之前就关闭。我们通过调用 end() 来执行关闭操作。
所以网页作者需要注意,除非您想作为反面教材出现在 Webkit 容错代码段的示例中,否则还请编写格式正确的 HTML 代码。 3.3 CSS 解析还记得简介中解析的概念吗?和HTML不同,CSS是上下文无关的语法,可以使用简介中描述的各种解析器进行解析。事实上,CSS 规范定义了 CSS 的词法和语法。让我们来看一些示例: 词法(词汇)是针对各个标记用正则表达式定义的:
“ident”是标识符 (identifier) 的缩写,比如类名。”name”是元素的 ID(通过”#”来引用)。 语法是采用 BNF 格式描述的。
解释:这是一个规则集的结构:
div.error 和 a.error 是选择器。大括号内的部分包含了由此规则集应用的规则。此结构的正式定义是这样的:
1.Webkit CSS 解析器Webkit 使用 Flex 和 Bison 解析器生成器,通过 CSS 语法文件自动创建解析器。正如我们之前在解析器简介中所说,Bison 会创建自下而上的移位归约解析器。Firefox 使用的是人工编写的自上而下的解析器。这两种解析器都会将 CSS 文件解析成 StyleSheet 对象,且每个对象都包含 CSS 规则。CSS 规则对象则包含选择器和声明对象,以及其他与 CSS 语法对应的对象。 ![]()
3.4 处理脚本和样式表的顺序1.脚本 网络的模型是同步的。网页作者希望解析器遇到 2.预解析Webkit 和 Firefox 都进行了这项优化。在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。请注意,预解析器不会修改 DOM 树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。 3.样式表另一方面,样式表有着不同的模型。理论上来说,应用样式表不会更改 DOM 树,因此似乎没有必要等待样式表并停止文档解析。但这涉及到一个问题,就是脚本在文档解析阶段会请求样式信息。如果当时还没有加载和解析样式,脚本就会获得错误的回复,这样显然会产生很多问题。这看上去是一个非典型案例,但事实上非常普遍。Firefox 在样式表加载和解析的过程中,会禁止所有脚本。而对于 Webkit 而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。 第四章 渲染树树构建 在 DOM 树构建的同时,浏览器还会构建另一个树结构:渲染树。这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的作用是让您按照正确的顺序绘制内容。
每一个呈现器都代表了一个矩形的区域,通常对应于相关节点的 CSS 框,这一点在 CSS2 规范中有所描述。它包含诸如宽度、高度和位置等几何信息。
元素类型也是考虑因素之一,例如表单控件和表格都对应特殊的框架。 4.1 渲染树和 DOM 树的关系 呈现器是和 DOM 元素相对应的,但并非一一对应。非可视化的 DOM 元素不会插入渲染树中,例如“head”元素。如果元素的 display 属性值为“none”,那么也不会显示在渲染树中(但是 visibility 属性值为“hidden”的元素仍会显示)。 有一些呈现对象对应于 DOM 节点,但在树中所在的位置与 DOM 节点不同。浮动定位和绝对定位的元素就是这样,它们处于正常的流程之外,放置在树中的其他地方,并映射到真正的框架,而放在原位的是占位框架。 ![]()
初始容器 block 为“viewport”,而在 Webkit 中则为“RenderView”对象。 4.2 构建渲染树的流程在 Firefox 中,系统会针对 DOM 更新注册展示层,作为侦听器。展示层将框架创建工作委托给 FrameConstructor,由该构造器解析样式(请参阅样式计算)并创建框架。 在 Webkit 中,解析样式和创建呈现器的过程称为“附加”。每个 DOM 节点都有一个“attach”方法。附加是同步进行的,将节点插入 DOM 树需要调用新的节点“attach”方法。 处理 html 和 body 标记就会构建渲染树根节点。这个根节点呈现对象对应于 CSS 规范中所说的容器 block,这是最上层的 block,包含了其他所有 block。它的尺寸就是视口,即浏览器窗口显示区域的尺寸。Firefox 称之为 ViewPortFrame,而 Webkit 称之为 RenderView。这就是文档所指向的呈现对象。渲染树的其余部分以 DOM 树节点插入的形式来构建。 请参阅关于处理模型的 CSS2 规范。 4.3 样式计算构建渲染树时,需要计算每一个呈现对象的可视化属性。这是通过计算每个元素的样式属性来完成的。 样式包括来自各种来源的样式表、inline 样式元素和 HTML 中的可视化属性(例如“bgcolor”属性)。其中后者将经过转化以匹配 CSS 样式属性。 样式表的来源包括浏览器的默认样式表、由网页作者提供的样式表以及由浏览器用户提供的用户样式表(浏览器允许您定义自己喜欢的样式。以 Firefox 为例,用户可以将自己喜欢的样式表放在“Firefox Profile”文件夹下)。 样式计算存在以下难点: 样式数据是一个超大的结构,存储了无数的样式属性,这可能造成内存问题。 例如下面这个组合选择器: div div div div{…} 。如果您要检查规则是否适用于某个指定的 元素,应选择树上的一条向上路径进行检查。您可能需要向上遍历节点树,结果发现只有两个 div,而且规则并不适用。然后,您必须尝试树中的其他路径。 应用规则涉及到相当复杂的层叠规则(用于定义这些规则的层次)。 让我们来看看浏览器是如何处理这些问题的:
1.共享样式数据Webkit 节点会引用样式对象 (RenderStyle)。这些对象在某些情况下可以由不同节点共享。这些节点是同级关系,并且: 这些元素必须处于相同的鼠标状态(例如,不允许其中一个是“:hover”状态,而另一个不是) 2.Firefox 规则树为了简化样式计算,Firefox 还采用了另外两种树:规则树和样式上下文树。Webkit 也有样式对象,但它们不是保存在类似样式上下文树这样的树结构中,只是由 DOM 节点指向此类对象的相关样式。 ![]()
样式上下文包含端值。要计算出这些值,应按照正确顺序应用所有的匹配规则,并将其从逻辑值转化为具体的值。例如,如果逻辑值是屏幕大小的百分比,则需要换算成绝对的单位。规则树的点子真的很巧妙,它使得节点之间可以共享这些值,以避免重复计算,还可以节约空间。 所有匹配的规则都存储在树中。路径中的底层节点拥有较高的优先级。规则树包含了所有已知规则匹配的路径。规则的存储是延迟进行的。规则树不会在开始的时候就为所有的节点进行计算,而是只有当某个节点样式需要进行计算时,才会向规则树添加计算的路径。 这个想法相当于将规则树路径视为词典中的单词。如果我们已经计算出如下的规则树: ![]()
假设我们需要为内容树中的另一个元素匹配规则,并且找到匹配路径是 B - E - I(按照此顺序)。由于我们在树中已经计算出了路径 A - B - E - I - L,因此就已经有了此路径,这就减少了现在所需的工作量。
还有如下规则:
为了简便起见,我们只需要填充两个结构:color 结构和 margin 结构。color 结构只包含一个成员(即“color”),而 margin 结构包含四条边。 ![]()
上下文树如下图所示(节点名 : 指向的规则节点): ![]()
假设我们解析 HTML 时遇到了第二个 标记,我们需要为此节点创建样式上下文,并填充其样式结构。 经过规则匹配,我们发现该 的匹配规则是第 1、2 和 6 条。这意味着规则树中已有一条路径可供我们的元素使用,我们只需要再为其添加一个节点以匹配第 6 条规则(规则树中的 F 节点)。 我们将创建样式上下文并将其放入上下文树中。新的样式上下文将指向规则树中的 F 节点。
现在我们需要填充样式结构。首先要填充的是 margin 结构。由于最后的规则节点 (F) 并没有添加到 margin 结构,我们需要上溯规则树,直至找到在先前节点插入中计算过的缓存结构,然后使用该结构。我们会在指定 margin 规则的最上层节点(即 B 节点)上找到该结构。 我们已经有了 color 结构的定义,因此不能使用缓存的结构。由于 color 有一个属性,我们无需上溯规则树以填充其他属性。我们将计算端值(将字符串转化为 RGB 等)并在此节点上缓存经过计算的结构。 第二个 元素处理起来更加简单。我们将匹配规则,最终发现它和之前的 span 一样指向规则 G。由于我们找到了指向同一节点的同级,就可以共享整个样式上下文了,只需指向之前 span 的上下文即可。 对于包含了继承自父代的规则的结构,缓存是在上下文树中进行的(事实上 color 属性是继承的,但是 Firefox 将其视为 reset 属性,并缓存到规则树上)。 p {font-family:Verdana;font size:10px;font-weight:bold} 因此概括来说,共享样式对象(整个对象或者对象中的部分结构)可以解决问题 1和问题 3。Firefox 规则树还有助于按照正确的顺序应用属性。 3.对规则进行处理以简化匹配样式规则有一些来源: 外部样式表或样式元素中的 CSS 规则
后两种很容易和元素进行匹配,因为元素拥有样式属性,而且 HTML 属性可以使用元素作为键值进行映射。 我们之前在第 2 个问题中提到过,CSS 规则匹配可能比较棘手。为了解决这一难题,可以对 CSS 规则进行一些处理,以便访问。 样式表解析完毕后,系统会根据选择器将 CSS 规则添加到某个哈希表中。这些哈希表的选择器各不相同,包括 ID、类名称、标记名称等,还有一种通用哈希表,适合不属于上述类别的规则。如果选择器是 ID,规则就会添加到 ID 表中;如果选择器是类,规则就会添加到类表中,依此类推。 我们以如下的样式规则为例:
第一条规则将插入类表,第二条将插入 ID 表,而第三条将插入标记表。
我们首先会为 p 元素寻找匹配的规则。类表中有一个“error”键,在下面可以找到“p.error”的规则。div 元素在 ID 表(键为 ID)和标记表中有相关的规则。剩下的工作就是找出哪些根据键提取的规则是真正匹配的了。
这条规则仍然会从标记表中提取出来,因为键是最右边的选择器,但这条规则并不匹配我们的 div 元素,因为 div 没有 table 祖先。 4.以正确的层叠顺序应用规则样式对象具有每个可视化属性一一对应的属性(均为 CSS 属性但更为通用)。如果某个属性未由任何匹配规则所定义,那么部分属性就可由父代元素样式对象继承。其他属性具有默认值。 如果定义不止一个,就会出现问题,需要通过层叠顺序来解决。
如果声明来自于“style”属性,而不是带有选择器的规则,则记为 1,否则记为 0 (= a) 一些示例:
4.4 渐进式处理Webkit 使用一个标记来表示是否所有的顶级样式表(包括 @imports)均已加载完毕。如果在附加过程中尚未完全加载样式,则使用占位符,并在文档中进行标注,等样式表加载完毕后再重新计算。 第五章 布局呈现器在创建完成并添加到渲染树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排。 HTML 采用基于流的布局模型,这意味着大多数情况下只要一次遍历就能计算出几何信息。处于流中靠后位置元素通常不会影响靠前位置元素的几何特征,因此布局可以按从左至右、从上至下的顺序遍历文档。但是也有例外情况,比如 HTML 表格的计算就需要不止一次的遍历 。 坐标系是相对于根框架而建立的,使用的是上坐标和左坐标。 布局是一个递归的过程。它从根呈现器(对应于 HTML 文档的 元素)开始,然后递归遍历部分或所有的框架层次结构,为每一个需要计算的呈现器计算几何信息。 根呈现器的位置左边是 0,0,其尺寸为视口(也就是浏览器窗口的可见区域)。 5.1 Dirty 位系统为避免对所有细小更改都进行整体布局,浏览器采用了一种“dirty 位”系统。如果某个呈现器发生了更改,或者将自身及其子代标注为“dirty”,则需要进行布局。 有两种标记:“dirty”和“children are dirty”。“children are dirty”表示尽管呈现器自身没有变化,但它至少有一个子代需要布局。 5.2 全局布局和增量布局全局布局是指触发了整个渲染树范围的布局,触发原因可能包括: 影响所有呈现器的全局样式更改,例如字体大小更改。 ![]()
5.3 异步布局和同步布局 增量布局是异步执行的。Firefox 将增量布局的“reflow 命令”加入队列,而调度程序会触发这些命令的批量执行。Webkit 也有用于执行增量布局的计时器:对渲染树进行遍历,并对 dirty 呈现器进行布局。 5.4 优化 如果布局是由“大小调整”或呈现器的位置(而非大小)改变而触发的,那么可以从缓存中获取呈现器的大小,而无需重新计算。 5.5 布局处理 布局通常具有以下模式: 5.6 宽度计算 呈现器宽度是根据容器块的宽度、呈现器样式中的“width”属性以及边距和边框计算得出的。
将由 Webkit 计算如下(BenderBox 类,calcWidth 方法):容器的宽度取容器的 availableWidth 和 0 中的较大值。availableWidth 在本例中相当于 contentWidth,计算公式如下:clientWidth()- paddingLeft()- paddingRight() 5.7 换行如果呈现器在布局过程中需要换行,会立即停止布局,并告知其父代需要换行。父代会创建额外的呈现器,并对其调用布局。 第六章 绘制在绘制阶段,系统会遍历渲染树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。绘制工作是使用用户界面基础组件完成的。 6.1 全局绘制和增量绘制 和布局一样,绘制也分为全局(绘制整个渲染树)和增量两种。在增量绘制中,部分呈现器发生了更改,但是不会影响整个树。更改后的呈现器将其在屏幕上对应的矩形区域设为无效,这导致 OS 将其视为一块“dirty 区域”,并生成“paint”事件。OS 会很巧妙地将多个区域合并成一个。在 Chrome 浏览器中,情况要更复杂一些,因为 Chrome 浏览器的呈现器不在主进程上。Chrome 浏览器会在某种程度上模拟 OS 的行为。展示层会侦听这些事件,并将消息委托给呈现根节点。然后遍历渲染树,直到找到相关的呈现器,该呈现器会重新绘制自己(通常也包括其子代)。 6.2 绘制顺序 CSS2 规范定义了绘制流程的顺序。绘制的顺序其实就是元素进入堆栈样式上下文的顺序。这些堆栈会从后往前绘制,因此这样的顺序会影响绘制。块呈现器的堆栈顺序如下: 6.3 Firefox 显示列表 Firefox 遍历整个渲染树,为绘制的矩形建立一个显示列表。列表中按照正确的绘制顺序(先是呈现器的背景,然后是边框等等)包含了与矩形相关的呈现器。这样等到重新绘制的时候,只需遍历一次渲染树,而不用多次遍历(绘制所有背景,然后绘制所有图片,再绘制所有边框等等)。 6.4 Webkit 矩形存储 在重新绘制之前,Webkit 会将原来的矩形另存为一张位图,然后只绘制新旧矩形之间的差异部分。 第七章 动态变化 在发生变化时,浏览器会尽可能做出最小的响应。因此,元素的颜色改变后,只会对该元素进行重绘。元素的位置改变后,只会对该元素及其子元素(可能还有同级元素)进行布局和重绘。添加 DOM 节点后,会对该节点进行布局和重绘。一些重大变化(例如增大“html”元素的字体)会导致缓存无效,使得整个渲染树都会进行重新布局和绘制。 第八章 渲染引擎的线程 渲染引擎采用了单线程。几乎所有操作(除了网络操作)都是在单线程中进行的。在 Firefox 和 Safari 中,该线程就是浏览器的主线程。而在 Chrome 浏览器中,该线程是标签进程的主线程。
第九章 CSS2 可视化模型9.1 画布根据 CSS2 规范,“画布”这一术语是指“用来呈现格式化结构的空间”,也就是供浏览器绘制内容的区域。画布的空间尺寸大小是无限的,但是浏览器会根据视口的尺寸选择一个初始宽度。 根据 www.w3.org/TR/CSS2/zindex.html,画布如果包含在其他画布内,就是透明的;否则会由浏览器指定一种颜色。 9.2 CSS 框模型CSS 框模型描述的是针对文档树中的元素而生成,并根据可视化格式模型进行布局的矩形框。 ![]()
每一个节点都会生成 0..n 个这样的框。 所有元素都有一个“display”属性,决定了它们所对应生成的框类型。示例:
默认值是 inline,但是浏览器样式表设置了其他默认值。例如,“div”元素的 display 属性默认值是 block。 9.3 定位方案 有三种定位方案: 如果值是 static 和 relative,就是普通流 9.4 框类型框尺寸 ![]()
![]() block 采用的是一个接一个的垂直格式,而 inline 采用的是水平格式。
![]()
inline 框放置在行中或“行框”中。这些行至少和最高的框一样高,还可以更高,当框根据“底线”对齐时,这意味着元素的底部需要根据其他框中非底部的位置对齐。如果容器的宽度不够,inline 元素就会分为多行放置。在段落中经常发生这种情况。 ![]()
9.5 定位1.相对相对定位:先按照普通方式定位,然后根据所需偏移量进行移动。 ![]()
2.浮动浮动框会移动到行的左边或右边。有趣的特征在于,其他框会浮动在它的周围。下面这段 HTML 代码:
显示效果如下: ![]()
3.绝对定位和固定定位这种布局是准确定义的,与普通流无关。元素不参与普通流。尺寸是相对于容器而言的。在固定定位中,容器就是可视区域。 ![]()
请注意,即使在文档滚动时,固定框也不会移动。 9.6 分层展示这是由 z-index CSS 属性指定的。它代表了框的第三个维度,也就是沿“z 轴”方向的位置。 这些框分散到多个堆栈(称为堆栈上下文)中。在每一个堆栈中,会首先绘制后面的元素,然后在顶部绘制前面的元素,以便更靠近用户。如果出现重叠,新绘制的元素就会覆盖之前的元素。
结果如下: ![]()
虽然红色 div 在标记中的位置比绿色 div 靠前(按理应该在常规流程中优先绘制),但是 z-index 属性的优先级更高,因此它移动到了根框所保持的堆栈中更靠前的位置。 |
|