现代浏览器的后台工作原理

本文翻译自http://taligarsiel.com/Projects/howbrowserswork1.htm 

注:原文有些罗嗦的内容直接隐去了,有兴趣的可以阅读原文。

还有一篇翻译过的:http://ux.sohu.com/topics/50972d9ae7de3e752e0081ff。 我想再翻译一遍,原因是,一是为了学习,翻译是个不错的途径;二是,也加入一些自己的理解。

介绍

浏览器基本结构

浏览器主体组件有:
1. 用户界面(User Interface): 包括工具栏、前进/后退按钮、书签菜单等;
2. 浏览器引擎(Browser engine):查询和管理渲染引擎的接口;
3. 渲染引擎(Rendering engine):负责显示请求内容。如,如果你请求的是HTML,它负责解析HTML和CSS并将解析结果显示在屏幕上。
4. 网路(Networking):执行网络请求,如HTTP请求等;
5.UI backend : 负责绘制基本的组件,如combo box和窗口。它只提供一个简单接口,各个平台要实现这些接口;
6. Javascript 解析器 
7. 数据存储(Data Storage) 这是一个持久层,浏览器需要存储各类数据,如cookies。HTML5定义了浏览器中的数据库"web databadse"。

现代浏览器的后台工作原理_第1张图片 

图1. 浏览器主组件

Chrome浏览器与其他浏览器不同,它是渲染引擎多进程的,每个网页在分离的进程中渲染。一个标签一个进程(虽然实际情况并不是这样,但是基本上可以这么理解)。

渲染引擎(The rendering engine)

渲染引擎,自然是用来渲染了(废话)。 默认情况下,渲染引擎可以显示HTML, XML文档和图片。也可以通过插件显示别的内容。例如PDF。本章不讨论插件,重点讨论HTNMl和图片的渲染。

各浏览器的渲染引擎

Firfox使用Gecko-Mozilia官方引擎。Safari和Chrome都使用Webkit。


主流程

渲染引擎从网络获取内容后开始工作。下面是它的主要工作流。


(解析HTML以构造DOM-> 渲染树构造 -> 布局渲染树 -> 绘制渲染树)

渲染引擎首先将解析HTML,将标签转换位DOM的节点树,这颗树成为”内容树“。同时还要解析风格,包括外部的CSS文件和内部的style元素。

这些风格定义信息和HTML的可见元素将床另外一棵树:渲染树。

渲染树包括矩形区域信息和可见属性,如颜色和位置。矩形区域将以正确的次序显示在屏幕上。

然后,渲染引擎进入布局过程。此过程将计算每个节点在屏幕上正确的坐标。然后是绘制,遍历渲染树上每个节点并绘制到UI backed层。

为了提升用户体验,渲染引擎会尽力尽早的显示出内容。不会等到HTML下载完成并解析和布局后。

主流程实例

Webkit的主流程:

现代浏览器的后台工作原理_第2张图片

Mozilla Gecko渲染引擎的主流程:

现代浏览器的后台工作原理_第3张图片


上两图说明Webkit和Gecko使用的技术明显不同,但是基本流程一样。

Gecko将可见元素组成的树为Frame树。每个元素都是一个frame。wekbit则使用"Render Tree"这个概念,它由"Render Objects"。Webkit使用"layout"描述元素定位,而Gecko则使用"Reflow"。 Webkit将DOM节点创建对应的"Render Tree"节点的过程为“Attachment"。

除此之外,Gecko在HTML和DOM树增加了一个额外层--“content sink",它是创建DOM元素的类工程。


HTML解析器

HTML解析器的功能是将HTML源码解析为一个解析树。

DOM

DOM即Document Object Model. 以对象化的方式保存HTML文档信息,并为javascript提供对外访问接口。

例如,

	
		

Hello World


将被转换为
现代浏览器的后台工作原理_第4张图片
w3c也将它标准化了, http://www.w3.org/DOM/DOMTR.  HTML的定义,参阅  http://www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html.

解析算法

通常的文本解析是不能满足HTML的解析要求的。why?
1. 语法较为宽松
2. 浏览器历来对不规范的语法采取包容的态度
3. 解析过程是可重入的。通常的源码都不会在解析过程中发生变化。但是HTML却可以,如包含"document.write"的脚本,就可以增加新的源码,改变解析输入的内容。
所以,HTML使用特有的解析方法。它包括两个部分 tokenization和 tree construction。
tokenization是词法分析器,将输入流转换为标记(token。这个过程,就好比将字符串流分成单词,每个单词按照它的含义创建出对应的对象。这个对象就是token,这个过程就是tokenization)。 这些token包括:开始标记(Start tag token),结束标记(end tag token),属性名(attribute name)和属性值(attribute value)。
tokenizer识别token,将token交割tree constructor,然后取下个字符,继续下个token的识别,直到输入流的结尾。
现代浏览器的后台工作原理_第5张图片

tokenization 算法

tokenization算法的输出是HTML token。 这个算法被描述为一个状态机的。每个状态从输入流中获取一个或者多个字符,并依据输入的字符来确定下个状态。 下个状态的确定,受到当前tokenization装和tree construction状态的影响。 
我们从一个简单的例子入手。
考虑如何tokenizing下面的HTML

	
		Hello world
	
初始状态为 "Data state", 当 '<' 字符到来时,状态变为"Tag open state" (标签开入状态)。 收到"a-z"字符时,会创建一个"开始标签 token", 状态也会变为 "Tag name state",直到收到">"字符。 这个状态下每个字符都会添加到token name中。在我们的例子中,我们创建了一个"html" token。
当">"字符达到,当前的token就会被提交,状态也相应的回到"Data state"。 ""标签的过程也是如此。

当"html"和"body"两个token被提交后,状态进入到"Data state"。此时,收到”Hello world"中的"H"标签,将创建一个字符token,直到""的"<"字符到来。然后,我们将提交这个字符token,然后又进入到了"Tag open state"。 因为下个字符是"/",因此,它创建了一个"结束标记token",并转换到"Tag name sate",直到收到">"字符。 然后新的结束标记token被提交,状态即重回"Data state"。""也将如此对待。
现代浏览器的后台工作原理_第6张图片

树构造算法

当解析被创建时,Document对象也会创建。他是DOM的根节点。在树构造阶段,DOM树将被修改并添加子元素。
每个由tokenizer提交的节点,都将被树构造器所处理。 对于每个token,都会有相应的DOM 元素对象被创建。此外,它会向DOM树添加“开发元素栈”(stack of open element)---这是为了纠正错误的嵌套关系和未关闭的标签。 
该算法也被描述为一个状态机。这个状态机被称为”插入模式“(insertion modes)。
还是这个例子:

	
		Hello world
	
在树构造阶段,输入的是产生于tokenization阶段的token序列。 初始模式为“initial mode",收到html token将进入"before html"模式,并重新处理该token。 此时将引起一个HTMLHtmlElement源被创建并设置为Document对象的根元素。
然后,状态变为"before head"。 当收到"body" token后,会隐藏创建一个HTMLHeadElemnt并添加到树中,尽管我们没有收到"head" token。此时,我们将模式转为“in head",再紧接着转为"after head"。 "body" token将被重新处理,此时,HTMLBodyElement被创建,然后插入到树中,并进入到 "in body"模式。
当字符token "Hello Workd"到达时,第一个字符将引导我们创建并插入一个“Text"节点,然后其他字符添加到该节点。
当收到"结束标记"token后,模式转为"after body"。 当收到 html的结束标记后,将转入 "after after body“模式。 当收到”结束文件“的token后,将结束解析过程。

现代浏览器的后台工作原理_第7张图片


解析完成后的动作

解析完成后,浏览器将DOM设置为可交互状态,并开始解析那些被"deferred"的脚本。这些脚本是在文档解析完成后执行的。
随后文档的状态设置为“complete",并且“load"事件被触发。

关于完整的算法,参阅 http://www.w3.org/TR/html5/syntax.html#html-parser

CSS 解析

不同于HTML,CSS的的语法更加自由,而且可以使用一种规范的解析方法。 CSS的规范定义了CSS的词法和语法   ( http://www.w3.org/TR/CSS2/grammar.html ).

来看个例子:
词法和语法都是通过正则表达式定义的
comment		\/\*[^*]*\*+([^/*][^*]*\*+)*\/
num		[0-9]+|[0-9]*"."[0-9]+
nonascii	[\200-\377]
nmstart		[_a-z]|{nonascii}|{escape}
nmchar		[_a-z0-9-]|{nonascii}|{escape}
name		{nmchar}+
ident		{nmstart}{nmchar}*
"ident" 是 identifier的缩写,"name"是一个元素的id (用'#'开头)

用BNF描述的语法:
ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;
selector
  : simple_selector [ combinator selector | S+ [ combinator selector ] ]
  ;
simple_selector
  : element_name [ HASH | class | attrib | pseudo ]*
  | [ HASH | class | attrib | pseudo ]+
  ;
class
  : '.' IDENT
  ;
element_name
  : IDENT | '*'
  ;
attrib
  : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
    [ IDENT | STRING ] S* ] ']'
  ;
pseudo
  : ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
  ;
其中,ruleset是这样一个结构
div.error , a.error {
	color:red;
	font-weight:bold;
}
div.error 和 a.error 是选择器(selector)。括号内部分,包含了一组规则(rule).  它的正式定义是
ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;
它表示,ruleset由一个selector或者多个可选的以空格或者逗号分割的多个selector组成,一个ruleset包含一对大括号,括号内是一个或者多个用分号分割的声明。 声明和选择器被后面的BNF文法所定义。

Webkit CSS解析器

webkit使用 Flex和 Bison生成CSS语法解析器。Bison可以创建一个自底而上的解析器。 Firefox则使用一个手动编写的自上而下的解析器。 无论何种情况,CSS文件都将被解析为StyleSheet 对象,每个对象包含CSS规则。每个规则包含选择子和声明集合。

现代浏览器的后台工作原理_第8张图片

脚本解析

处理脚本和样式单的顺序

web的模型是异步的。作者认为当遇到script标签时,脚本的解析和执行是立即开始的。文档的解析会被挂起,直到脚本被执行。
如果脚本是来自外部资源,那么,首先要从网络获取该资源。这个过程同样是异步的,但是解析过程还是会被挂起,直到脚本被获取并执行成功。 这个模型被HTML4和5定义并执行了很多年。 网页的作者可以将脚本设置为"defer",这样,文档的解析将不会被挂起,这些脚本也将在解析完成后被执行。 HTML5添加了一个选项,可以标记脚本为异步的。

预测解析

webkit和Gecko都做了这方面的优化。当执行脚本时,网页线程将继续解析生育的文档,并找到其他需要网络加载资源并加载他们。这样资源可以并行的加载,并提升速度。注意:预测解析并不会更改DOM树,只是将解析外部资源引用,例如外部的脚本、样式表和图片。

样式表

样式表使用不同的模型。从概念上讲,样式表并不改变DOM树,因此,没有理由去等待样式表并停止文档解析。但是,如果脚本在文档解析阶段访问了样式信息,就不同了。如果样式没有加载和解析,那么脚本将获取错误的内容并引起很多问题。这个问题看起来不常见实际却很常见。 
Gecko的做法是在style加载和解析时,阻塞所有脚本的执行;而webkit则仅在脚本需要访问某个未加载样式时才会阻塞。

渲染树的构造

当DOM树被创建后,浏览器创建了另外的树:渲染树。这个树是将所有可见元素以正确的方式排列,以便让他们能够被显示。

firefox称渲染树中的元素为"frames"。webkit则称为 "render object"

一个渲染元素直到如何布局和绘制它自己及其子孙。

webitk的RenderObject类是所有渲染元素的基础类,定义如下
class RenderObject{
	virtual void layout();
	virtual void paint(PaintInfo);
	virtual void rect repaintRect();
	Node* node;  //the DOM node
	RenderStyle* style;  // the computed style
	RenderLayer* containgLayer; //the containing z-index layer
}
每个渲染元素代表一个矩形区域,这个区域被 称为CSS box,在CSS2标准中被定义。
box的类型,受到对应元素的"display"风格的影响。下面是webkit的代码,展示了display属性如何决定盒子的类型
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
    Document* doc = node->document();
    RenderArena* arena = doc->renderArena();
    ...
    RenderObject* o = 0;

    switch (style->display()) {
        case NONE:
            break;
        case INLINE:
            o = new (arena) RenderInline(node);
            break;
        case BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case INLINE_BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case LIST_ITEM:
            o = new (arena) RenderListItem(node);
            break;
       ...
    }

    return o;
}
元素的类型也是考虑因素之一。如,form和table就拥有特殊的类型。

在webkit中,如果一个元素想创建特殊的渲染器,就通过重载createRenderer方法。
渲染器对象通过一个style对象,引入一些非位置相关的样式信息。

渲染树关联到DOM树

渲染树上的元素与DOM元素是相对应的,但不是一一对应。非可见DOM元素将不会在渲染树中有对应者。比如,“head”元素。那些display属性为“none”的元素,也不会出现(但是,”hiddeln“元素却是出现在渲染树中的)。

有的DOM元素对对应多个渲染元素。通常是一些复合元素,他们无法用一个单一的矩形来描述。例如,”select“元素有3个渲染元素,一个用作显示元素,另外一个用作显示下拉列表框,一个用作按钮。同理,当一个文本被分割为多行的时候,因为宽度值只够一行使用,因此,新的行将被作为新的渲染元素添加到后面。
再有,按照CSS标准,一个内联元素只能包含块元素或者内联元素,当需要混合式,一个包装内联元素的隐藏的块元素会被创建。

有些渲染元素在树中的位置并不和它对应的DOM元素在DOM树中的位置一样。浮动和绝对定位的元素是独立于流的,被放置在树的不同位置,并映射到真正的元素。 (Some render objects correspond to a DOM node but not in the same place in the tree. Floats and absolutely positioned elements are out of flow, placed in a different place in the tree, and mapped to the real frame. A placeholder frame is where they should have been.)
现代浏览器的后台工作原理_第9张图片

渲染树构造流

在Firefox中,构造器注册为监听DOM更新的对象。”FrameConstructor“是用于渲染元素创建的,同时解析样式。

在Webkit中,创建渲染元素和解构样式表的过程称为”依附(Attachment"。每个DOM节点都有一个“attach”方法。“依附”是异步的,当节点被插入树时,DOM调用新节点的“attach”方法。

html和body标签在构造时,生成渲染树的根。跟渲染元素即CSS标准中所说的容器块(containning block),这是包含其他所有块的顶层块。它的大小就是视口--浏览器窗口的显示区域。 Firefox称为“ViewPortFrame”, 而webkit称为“RenderView”。 document对象包含了根渲染元素,树的其余部分在DOM节点插入时创建。
请参阅CSS2章节   http://www.w3.org/TR/CSS21/intro.html#processing-model

样式计算

渲染树需要计算每个渲染元素的可见属性。样式包括来自于各种途径的样式表,和元素中的内联样式(如元素的“bgcolor”属性)。

样式表来源包括浏览器的默认样式表、网页作者提供的样式表以及用户样式表--这些是来自浏览器的使用者(浏览器允许用户定义他们喜欢的风格。例如,在firefox中,这些存储在“Firefox Profile”文件夹中)。

样式的计算有如下困难:
1. 样式数据众多,牵扯大量样式属性,会引起内存大量消耗;
2. 为每个元素查找和匹配样式规则会消耗大量的性能且不能优化。为每个元素遍历整个规则列表是个沉重的任务。那些具有复杂结构的选择器可能会让匹配过程经历多个尝试后才能找到正确的结果。如
div div div div{
...
}
这个规则将匹配有3个“div"祖先的div元素。嘉定你想检查一个给定的div元素是否符合这个规则,就得向上一个一个的检验。
3. 需要处理那些具有继承关系的复杂的级联式负责。

让我看看浏览器如何处理这些问题:

共享样式数据

在某些情况下,webkit多个节点会共享同一个样式对象(RenderStyle)。这些节点一般是兄弟或者堂兄弟节点,或者是:
1. 具有同一种鼠标状态的节点(如,”:hover"状态仅有一个元素有);
2. 没有Id属性的元素
3. 相同标签的元素
4. 具有相同class属性的元素
5. 具有同一组被映射属性的元素
6. link状态一致的元素
7. 具有焦点状态的元素
8. 受属性选择器影响的元素除外
9. 没有内联样式的元素
10. 必须没有任何兄弟选择器。 这种情况下,Webkit会简单的禁止整个文档使用共享机制。这种选择器包括 "+" 选择和类似":first-child" 和":last-child"这样的选择器。

firefox的规则树

firefox用两个树来简化风格计算--规则树和样式上下文树。 webkit也使用样式对象,但是他并没有存储为样式上下文树,只有DOM节点能够支出他的关联样式。
现代浏览器的后台工作原理_第10张图片

样式上下文(style contexts)包含了最终的值。这些值是通过正确的顺序匹配规则时创建,将它们由逻辑值转换为具体值。 如,如果逻辑值是屏幕的百分比,那么他将这个逻辑值计算并转换为实际的屏幕单位。 规则树的思路很巧妙,他让节点共享了样式值,避免了多次计算,同时节省了控件。

所有被匹配的规则存储在树中。规则树中,节点层次越深,其优先级越高。规则的构造是延迟的。只有当样式被需要时,他们才会被计算并插入到规则树中。

这个思路是将树的路径看做词汇表中的一组词汇。

假如我们已经有一个规则树:
现代浏览器的后台工作原理_第11张图片
假定我们需要匹配
现代浏览器的后台工作原理_第12张图片
我们需要为内容树中的另外一个元素匹配规则,并且找到了规则时B-E-I。 在规则树中我们已经计算出了规则路径A-B-E-I-L。此时我们就无需做更多了。

结构分离

样式上下文被分为若干结构体。这些结构体包含了一组特定的风格,就像border或者color。结构体包含两种属性:可继承的和不可继承的。对于继承属性,除非元素自己定义了他们的值,否则属性的值将来自于元素的父节点;对于非继承属性(称为"reset"属性),如果元素没有定义,他们就使用默认值。

规则树帮助我们缓存整个树(包括计算后的最终值),他的思路是,如果下层元素没有提供某个结构的定义,那么在他的上层节点就能找到被缓存的结构。

使用规则树来计算样式上下文

直接上例子(跟原文不一样,感觉原文有些绕,直接将精髓找出来)

	
		

this is a big error this is also a very big error error

another error
有如下样式规则
1.	div {margin:5px;color:black}
2.	.err {color:red}
3.	.big {margin-top:3px}
4.	div span {margin-bottom:4px}
5.	#div1 {color:blue}
6.	#div 2 {color:green}
firefox将生成规则树( A,B,C,D等表示节点的名称,是为了后文记述方便,实际不存在,数字表示第几个规则)
现代浏览器的后台工作原理_第13张图片

内容树,看起来像这样(“:”后字母表示对应规则树上的节点)
现代浏览器的后台工作原理_第14张图片
来叙述一下这个过程:
当解析到html标签和body标签时,没有任何匹配规则,因此,他们使用默认规则(A),并将这个规则作为规则树的根。
当解析到第一个div标签是,div标签匹配的规则是(1,2,5,注意顺序表示优先级从低到高),此时规则树中不存在这样的路径,于是,节点B被创建,并添加到节点A下,依次创建了节点C和D。
当解析到div下的p节点时,他没有对应的规则,因此使用节点A作为自己的规则。
当解析到div下的第一个span时,他匹配到了(3,4, class规则更高一些),该路径不存在,创建节点(E,G)。当匹配第二个span时,同样匹配到了规则(3,4),这个路径已经存在,因此直接使用节点G。
当解析到第二个div时,匹配的规则时(1,2,6),路径1,2已经存在,因此,直接创建节点F,就得到了1,2,6的路径。

对于那些包含可继承属性的规则,他们会使用父元素的值。
例如,如果增加一条规则
p {font-family:Verdana;font size:10px;font-weight:bold} 
作为p的子元素的span元素,将继承p定义的字体风格。(color属性实际上也是继承的,但是firefox将它看做不可继承属性对象)。

在webkit中没有规则树,它需要进行4次匹配。第一次匹配不重要但优先级高的属性(其他属性依赖于这些属性,如display)并应用,然后是高优先级的重要属性,再次是普通优先级非重要属性,最后是普通优先级中华要属性。这意味着属性将以正确的顺序多次被匹配和处理。

总之,样式对象共享可以解决问题1和3。firefox的规则树也可以帮助属性以正确的顺序被处理。

维护规则以便简化匹配过程

样式来自几个地方:
  • CSS样式表,包括外部样式表文件和style元素定义;
p {color:blue}
  • 内联样式属性

  • HTML的可见属性

后两种非常容易处理,因为他们只对特定元素有效。

如前文所提到的问题2,CSS规则的匹配很复杂。为解决这个问题,规则将被组织成一种更容易访问的方式。

当解析完样式表后,按照选择器,规则将被添加到几个hash表中。按照id,class,标签名和通用表来分类。如果选择是id,那么该规则将被加入到id表;如果选择器是class,则它会被加入到class表,以此类推。都不符合的回加入到通用表中。
这种方法让匹配更加容易。他不需查找每个生命,只需要从表中查找即可。这种优化可以处理95%以上的规则。

让我们看看例子
p.error {color:red}
#messageDiv {heigh
div {margin:5px}
 第一个规则,将被插入到class表。第二个将被插入到id表,第三个将被插入到标签表。 
   
 
   
 
  
对于下面这段HTML代码

an error occurred

this is a message
首先,试图查找匹配p元素的规则。class表中包含“error”key的"p.error"规则被发现。 
div元素将从id表中和标签表中找到规则。

如果div的规则是
table div {margin:5px}
他讲被加入到标签表中,以最右边的选择器为key(div)。但是对于上面的HTML代码,他将不会被选中,因为我们的div没有一个“table"祖先元素。

Webkit和Firefox都有这种机制。

以正确的顺序匹配规则

样式对象包含了每个可见元素对应的属性。如果所匹配的规则中没有包含任何属性,那么元素将使用父元素的样式对象。其他的元素的属性则使用默认值。

问题在于当一个属性有多个定义时选择哪个?

样式的级联次序
一个样式定义可以出现在多个样式表中,而且,在一个样式表中也可以多次出现。 这意味着,规则的匹配顺序非常重要。这种属性被称为“级联“顺序。

按照CSS2的定义,级联属性是(从低到高)
1. 浏览器的定义
2. 用户普通的定义
3. Page作者的普通定义
4. 作者重要定义
5. 用户的重要定义。
浏览器的定义优先级最低,用户的定义只有标记为“重要”的情况下,才会覆盖作者的定义。相同次序的定义将按照其特殊性进行排序和存储。
HTML可见属性也被转换到CSS定义,它的优先级同于作者的优先级。

特殊性
选择其的特殊性,被定义在 CSS2 specification,如下:
  • 如果直接定义在元素的style属性,那么,记为 a= 1, 否则记为a=0
  • 计算选择器中ID的数量,记为b的值
  • 计算选择器中的其他属性和伪类的数量,记为c的值
  • 计算选择器中的元素名称和伪元素的数量,记为d
联合这4个数量值a-b-c-d(通过大树进制的方式)给出其优先级

你所使用的进制,取决于你的最高级的数量。例如,如果a=14,你使用16进制。而如果a=17,你就需要17进制了。这种情况,在像这样的选择器中“html body div div p ..." (17个标签)的条件下容易出现这种情况。

例如
 *             {}  /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
 li            {}  /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
 li:first-line {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul li         {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
 h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
 ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
 li.red.level  {}  /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
 #x34y         {}  /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
 style=""          /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */

对规则进行排序

当规则被匹配后,他们会按照级联规则进行排序。 webkit使用冒泡排序法对小列表,而使用合并排序法针对大列表。 webkit通过重载“>"操作符来实现排序:
static bool operator >(CSSRuleData& r1, CSSRuleData& r2)
{
    int spec1 = r1.selector()->specificity();
    int spec2 = r2.selector()->specificity();
    return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2; 
}

平滑处理

webkit使用一个变量来标记所有的顶层样式表(包括"@imports")是否被加载。如果样式在使用时未被加载,那么一个占位符将被使用,并在文档中被记录。一旦样式表被加载,他们就会被重新计算。

布局

当渲染元素被创建然后被添加到树中的时候,它并没有位置和大小。计算这些值的过程叫做"layout(布局)“或"reflow"。

HTML使用基于布局模型的流,这意味者,它可以在大多数事件内以单一的方式计算坐标位置。"在流中(in the flow)“早些的元素先进行排序,晚些的元素后续排序。因此,布局可以处理从左导游,从上大小的排序。但也有例外,如 HTML的table可能会需要多于一个的过程。

坐标系统依赖于根元素,top和left坐标将被使用。

布局是递归的。它开始于和HTML文档对应的根渲染元素。它按照元素的层级进行递归,计算每个元素的位置信息。

根元素的位置是开始于0,0,大小为视口,视口代表了浏览器窗口中可见部分。

脏标志系统

为了避免小改动引起全部布局,浏览器使用了一个脏标志系统。如果渲染元素发生改变或者其子孙发生改变,它就会添加一个脏标志,表示需要被布局。

有两类脏标志“dirty"和"children are dirty"。脏子孙标记表示仅仅需要改变子孙而不是它自己。

全局和自增布局

整个渲染树被触发的布局称为“全局布局”。它发生于:
1.全局演示改变后,如字体大小;
2. 窗口大小改变后。

布局可以是自增的,只有脏元素才会被布局(这可能会引起额外布局的危险)。
当渲染元素变脏后,就会触发自增布局(当然是异步的)。例如,当新的渲染元素被添加到渲染树中后。
现代浏览器的后台工作原理_第15张图片

同步和异步布局

自增布局是异步的。firefox通过对"reflow commands"进行排队和一个调度器批量执行这些命令,实现自增布局。Webkit也使用了一个定时器去执行子正句柄。 

脚本查询样式信息时,如"offsightHeight",就会触发一个同步布局

全局布局通常按照同步布局比触发。

有时,在布局初始化完成后,布局被作为一个回调被除非,例如,当滚动位置发生变化后。

优化

当布局被“resize”或者渲染其元素位置变化(不是大小)触发时,渲染器的大小将从缓存中取得,不会重复计算。 在一些情况下,只有子树被改变时,仅仅从子树开始布局,而不是其根节点。这种情况发生在一些不会影响到周边变化的局部改变上,如,在text field中增加文本。

布局过程

布局通常有如下过程
  1. 父渲染元素决定它自己的宽度;
  2. 父元素遍历子元素,并
    1. 确定子元素的位置(x,y)
    2. 如果需要,调用元素的布局,最终得到子元素的高度
  3. 父对象使用子元素的总体高度以及它自己的margins和paddings的高度,作为它自己的高度
  4. 清除脏标记
firefox使用“state"对象(nsHTMLReflowState)作为布局的参数。state参数中包含了父的宽度。
FireFox布局输出的是"metrics"对象(nsHTMLReflowMetrcs),它将包含渲染后的高度。

宽度计算

渲染器的宽度通过容器块的宽度、渲染器的“width”属性,margins和边框。

例如,下面div的宽度是
  • webkit的计算过程(class RenderBox的calcWidth方法)
  • 计算容器允许的最大宽度。本例中的允许宽度是内容的宽度: clientWidth() - paddingLeft() - paddingRight() clientWidth和clientHeight代表了一个对象除了边框和滚动条外的内部值。
  • 根据元素的width属性,计算出容器的30%
  • 添加水平边框和paddings的值
这个计算出的是“首先宽度”。然后计算宽度的最大和最小值。
如果首选宽度大于最大宽度,那么最大宽度将使用;同理,如果小于最小宽度,那么最小宽度被使用。

这些值会被缓存。

断行

如果渲染器发现布局中需要断行,它会停止处理并通知给父节点。父节点将创建一个附加的渲染器并继续进行布局。

绘制

在绘制阶段,渲染树将被遍历,并且调用“paint”方法。绘制过程将使用UI的基本组件。更多参见UI相关章节。

全局和自增绘制

和布局类似,绘制也有全局--整个树被绘制和自增绘制。在自增绘制中,一些渲染其的变化不会影响到全局树。这种渲染器将它自己在屏幕上的矩形设置为无效的。这会引起OS去查询所谓的“脏区域”并产生“paint”事件。OS会将几个重叠的区域合并为一个区域。
在chrome中,这个过程更加负责,因为,它在不同进程处理渲染过程。
Chrome模仿了OS的行为,并对它有所扩展。

绘制顺序

CSS2定义了绘制顺序-- http://www.w3.org/TR/CSS21/zindex.html 元素的实际的绘制属性被按照栈的方式放在栈上下文中。因为栈是从底向上绘制的,它将影响到绘制的结果。一个块元素的绘制栈序是
  1. 背景色
  2. 背景图片
  3. 边框
  4. 子对象
  5. 轮廓

Firefox显示列表

firefox遍历渲染树并建立一个显示列表。它包括了渲染器必须的矩形区域和正确的顺序。
这种方法只需要遍历一次树而可以多次重复绘制。
firefox优化了那些不需要显示的元素--如被不透明元素覆盖的其他元素的。

webkit的矩形存储

webkit在重绘前,存储老区域为一个位图,它只绘制新老区域的不同部分。

动态变化

浏览器试图为每个改变做最少的动作。对于一个元素颜色的改变,仅会重绘该元素;对于一个元素位置的改变,将会引起对该元素及其子元素以及可能的兄弟元素的重布局和重绘。添加一个DOM元素,会引起针对该元素的布局和重绘。大的变化,如对HTML元素增加字体大小会引起对整个树的缓存、布局和绘制的改变。

渲染引擎线程

渲染引擎是单线程的。除了网络外,几乎所有事情都发生在单个线程。对于firefox和safari,他们就是浏览器的主线程。在chrome中,它是一个tab进程的主线程。
网络操作可以被几个并行的线程处理。并行链接的通常是有限制的(通常是2-6个,例如Firefox 3是6个)。

事件循环

浏览器主循环就是一个事件循环。firefox的主循环如下
while (!mExiting)
    NS_ProcessNextEvent(thread);

CSS2可视模型

Canvas

按照CSS标准, canvas被描述为一个“结构化数据被描述的场所“ -- 这是浏览器主要绘制内容的地方。
canvas是一个逻辑上无限延展的控件,而浏览器通常为它定义一个初始的大小,即视口的大小。

按照 http://www.w3.org/TR/CSS2/zindex.html 除非名曲指出浏览器颜色,否则canvas是透明的。

CSS Box模型

CSS Box模型描述了一个存放文档树内容的矩形盒子。
每个盒子有内容区域、可选的环绕填充(padding),边框和留白(margin)
现代浏览器的后台工作原理_第16张图片
每个节点都会产生0到N个这样的盒子。
元素的“display"属性值决定了他们产生什么样的盒子。
例如
block - 产生一个块盒子
inline - 产生一个或者多个内联盒子
none - 不产生盒子

默认情况下都是内联盒子,但是浏览器自身的样式会改变它的默认值。如div元素的默认值就被改为block。
你可以从这里获取默认的样式: http://www.w3.org/TR/CSS2/sample.html

定位模式

有3种模式:
1. 普通 - 按照元素在其文档中的位置进行定位;
2. 浮动 - 元素先于普通定位模式而定位,他们总是被尽可能的向左或者右来移动定位;
3. 决定 - 元素将按照它自己的位置信息放置在与DOM树无关的位置。

它通过postion属性和float属性来觉得
  • static和relative标记一个普通定位额
  • absolute和fixed标记一个觉得定位
在static定位中,如果没有postion定义,那么默认的定位将被启动。在其他模式下,作者可以指出他们的位置--top,bottom,left,right。

box的位置将被如下决定:
  • box的类型
  • box的大小
  • 定位模式
  • 外部信息-如图片大小和屏幕大小等

box类型

块box:像一个块,单独占用一行,在浏览器窗口有自己的区域
现代浏览器的后台工作原理_第17张图片

内联box:没有自己的块,但是包含在一个块内
现代浏览器的后台工作原理_第18张图片

块按照垂直的方法一层一层的排列下来,而内联的则按照水平的块排列

现代浏览器的后台工作原理_第19张图片

定位

相对定位

相对于前一个进行定位,可以指出偏移
现代浏览器的后台工作原理_第20张图片

浮动定位

浮动定位将元素尽可能的移动到左边或者右边。

Lorem ipsum dolor sit amet, consectetuer...

看起来就是:
现代浏览器的后台工作原理_第21张图片

决定和固定定位

现代浏览器的后台工作原理_第22张图片
固定定位下,即使滚动也不会改变其位置。

CSS的z-index属性确定了层。它表示一个box的第三维信息。称为"z轴“。

盒子被花费到不同的栈中(称为栈上下文)。每个栈按照从低向上的方式来绘制。

栈的排列按照z-index属性。有z-index属性的盒子放在本地栈,而视口则是一个外部栈。
例如


  

结果是
现代浏览器的后台工作原理_第23张图片
虽然绿色div后于红色div被定义,但是由于z-index的关系,红色div覆盖掉了绿色div的区域。

参考文献

  1. Browser architecture
    1. Grosskurth, Alan. A Reference Architecture for Web Browsers. http://grosskurth.ca/papers/browser-refarch.pdf.
  2. Parsing
    1. Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (aka the "Dragon book"), Addison-Wesley, 1986
    2. Rick Jelliffe. The Bold and the Beautiful: two new drafts for HTML 5. http://broadcast.oreilly.com/2009/05/the-bold-and-the-beautiful-two.html.
  3. Firefox
    1. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers. http://dbaron.org/talks/2008-11-12-faster-html-and-css/slide-6.xhtml.
    2. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers(Google tech talk video). http://www.youtube.com/watch?v=a2_6bGNZ7bA.
    3. L. David Baron, Mozilla's Layout Engine. http://www.mozilla.org/newlayout/doc/layout-2006-07-12/slide-6.xhtml.
    4. L. David Baron, Mozilla Style System Documentation. http://www.mozilla.org/newlayout/doc/style-system.html.
    5. Chris Waterson, Notes on HTML Reflow. http://www.mozilla.org/newlayout/doc/reflow.html.
    6. Chris Waterson, Gecko Overview. http://www.mozilla.org/newlayout/doc/gecko-overview.htm.
    7. Alexander Larsson, The life of an HTML HTTP request. https://developer.mozilla.org/en/The_life_of_an_HTML_HTTP_request.
  4. Webkit
    1. David Hyatt, Implementing CSS(part 1). http://weblogs.mozillazine.org/hyatt/archives/cat_safari.html.
    2. David Hyatt, An Overview of WebCore. http://weblogs.mozillazine.org/hyatt/WebCore/chapter2.html.
    3. David Hyatt, WebCore Rendering. http://webkit.org/blog/114/.
    4. David Hyatt, The FOUC Problem. http://webkit.org/blog/66/the-fouc-problem/.
  5. W3C Specifications
    1. HTML 4.01 Specification. http://www.w3.org/TR/html4/.
    2. HTML5 Specification. http://dev.w3.org/html5/spec/Overview.html.
    3. Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification. http://www.w3.org/TR/CSS2/.
  6. Browsers build instructions
    1. Firefox. https://developer.mozilla.org/en/Build_Documentation
    2. Webkit. http://webkit.org/building/build.html

你可能感兴趣的:(webikit)