前言

为方便读者阅读,贴出以下参考链接,在此感谢前辈笔记。

浏览器解析过程分析:前端必读:浏览器内部工作原理_知识库_博客园 (cnblogs.com)

浏览器工作过程分析:万字详文:深入理解浏览器原理 - 知乎 (zhihu.com)

1. 树的构建流程

image.png

HTML 和 CSS 的页面解析是从上之下的,线程的。文档流从上往下,若先碰到了 CSS,那么开始解析,接着不被阻塞的继续向下解析 HTML,这样既符合并行解析,又符合文档流自上往下解析规则。浏览器先下载 HTML 文件开始解析,遇到 CSS 标签就开始下载 CSS 并解析,这个过程不会阻塞 DOM 的构建。最后 DOM 树和 CSS 规则树生成渲染树,HTML 解析完成。

注意几点:

  1. CSS 加载不会阻塞 DOM 树的解析
  2. CSS 加载会阻塞 DOM 树的渲染(不阻塞解析),即合并成渲染树的过程,因为样式可能会导致重排重绘
  3. CSS 加载会阻塞后面 JS 语句的执行

解析、渲染这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的 html 都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时还可能通过网络下载其余内容。

1.1 HTML Parser

1.1.1 解析

解析的目的是:将文档转换为具有一定意义的结构,编码时可以理解和使用的东西。解析的结果通常是表达文档结构的节点树,称为解析树或语法树。

解析的目的在于让目标更接近于需要使用的形式,或更具体的描述一些信息。如高级语言先转换为汇编等低级语言,最后再转换成机器码,又或者是家具的运输与组装,零件形式更接近于运输模式,成品形式更接近于使用模式,由成品拆分成零件就是使其更接近于运输模式,同样的道理将文档解析成另外一个格式会更适合某一个场景的使用。

如解析“2+3-1”这个表达式,可能返回这样一棵树。

image.png

解析过程与翻译过程类似,需要具备两个条件,字典和语法规则。

文法(Grammars):解析基于文档依据的语法规则——文档的语言或格式。每种可被解析的格式必须具有由词汇及语法规则组成的特定的文法,称为上下文无关文法。简单来说就是由词汇表、语法规则组成的对某一个格式的限制,与当前文件内容无关。

文法不等于语法规则,文法是词汇表和语法规则的结合,如语法规则定义为“字后面跟着表示事物的名词,字跟在动词后面,字后面跟着形容词”,词汇表则有“谁,得,的,地,花,走,快”,文法规则就是词汇表和语法规则的结合,包含任何一个可能选择,如“谁的花,走得快,的,得,地”等。

解析可以分成两个子过程:

  • 词法分析
  • 语法分析

词法分析是将文档中的字词按照一定地规则(词汇表)分成符号,符号对应的便是词汇表(基本有效单元的集合)中的某一项。对于人类语言来说,它相当于我们字典中出现的所有单词。

语法分析是根据语法规则匹配词法分析后的符号,生成特定状态的过程。

词法分析器(有时也叫分词器)负责将输入分解为合法的符号,语法分析器则根据语言的语法规则分析文档结构,从而构建解析树,词法分析器知道怎么跳过空白和换行之类的无关字符。

image.png

解析过程是迭代的,语法分析器从词法分析器处取到一个新的符号,并试着用这个符号匹配一条语法规则,如果匹配了一条规则,这个符号对应的节点将被添加到解析树上,然后语法分析器分析下一个符号。如果没有匹配到规则,语法分析器将在内部保存该符号,并从词法分析器取下一个符号,直到所有内部保存的符号能够匹配一项语法规则。如果最终没有找到匹配的规则,解析器将抛出一个异常,这意味着文档无效或是包含语法错误。

词法分析器和语法分析器的工作是同步的,正如组装家具时,找到一个一个零件,根据说明书进行组装,如果没有找齐零件,是无法组装某一个个体的。

1.1.2 转换

转换对工作人员来说是一个隐式的过程,正如在浏览器开发者控制台运行一些代码、打印日志信息一样,开发者可能感受不到转换到机器码的过程。但是编写的源代码并不是最终的目标,底层机器码才是机器识别、运行的格式。

很多时候,解析树并不是最终结果。解析一般在转换中使用——将输入文档转换为另一种格式。编译就是个例子,编译器在将一段源码编译为机器码的时候,先将源码解析为解析树,然后将该树转换为一个机器码文档。

简单来说转换过程在编译运行时就存在,将源代码转换为机器码,转换时是无感的。

image.png

详细的例子可以看看:前端必读:浏览器内部工作原理_知识库_博客园 (cnblogs.com),如不熟悉抽象语法树的概念,可以看看:手把手带你入门 AST 抽象语法树 - 掘金 (juejin.cn)

1.1.3 深入

HTML解析器(HTML Parser):HTML解析器的工作是将html标识解析为解析树。

HTML文法定义(The HTML grammar definition):W3C组织制定规范定义了HTML的词汇表和语法。

非上下文无关文法(Not a context free grammar):正如在解析简介中提到的,上下文无关文法的语法可以用类似BNF的格式来定义。

HTML 有一个正式的格式定义—— DTD(Document Type Definition文档类型定义),但它并不是上下文无关文法,HTML 更接近于 XML,现在有很多可用的xml 解析器,html 有个 xml 的变体——xhtml,不同在于 html 更宽容,它允许忽略一些特定标签,有时可以省略开始或结束标签。总的来说,它是一种 soft 语法,不像 xml 呆板、固执。

显然,这个看起来很小的差异却带来了很大的不同。一方面,这是 html 流行的原因——它的宽容使 web 开发人员的工作更加轻松,但另一方面,这也使很难去写一个格式化的文法。所以,html 的解析并不简单,它既不能用传统的解析器解析,也不能用 xml 解析器解析。

HTML DTD

html 适用 DTD 格式进行定义,这一格式是用于定义 SGML 家族的语言,包括了对所有允许元素及它们的属性和层次关系的定义。正如前面提到的,html DTD 并没有生成一种上下文无关文法。DTD 有一些变种,标准模式只遵守规范,而其他模式则包含了对浏览器过去所使用标签的支持,这么做是为了兼容以前内容。最新的标准 DTD 在http://www.w3.org/TR/html4/strict.dtd

DOM

解析输出的树也就是解析树,是由 DOM 元素及属性节点组成的。DOM 是文档对象模型的缩写,它是 html 文档的对象表示,作为 html 元素的外部接口供 JS 等调用。DOM 和标签基本是一一对应的关系。

解析器类型(Types of parsers)

有两种基本的解析器——自顶向下解析及自底向上解析。比较直观的解释是,自顶向下解析,查看语法的最高层结构并试着匹配其中一个;自底向上解析则从输入开始,逐步将其转换为语法规则,从底层规则开始直到匹配高层规则。

来看一下这两种解析器如何解析 “2+3-1” 的例子:自顶向下解析器从最高层规则开始——它先识别出“2+3”,将其视为一个表达式,然后识别出 “2+3-1” 为一个表达式(识别表达式的过程中匹配了其他规则,但出发点是最高层规则)。自底向上解析会扫描输入直到匹配了一条规则,然后用该规则取代匹配的输入,直到解析完所有输入。部分匹配的表达式被放置在解析堆栈中。

自顶向下与递归过程类似,从整体开始逐步检查子问题,而自底向上与递推过程类似,从子问题开始推向整体。

自动化解析(Generating parsers automatically)

解析器生成器这个工具可以自动生成解析器,只需要指定语言的文法——词汇表及语法规则,它就可以生成一个解析器。创建一个解析器需要对解析有深入的理解,而且手动的创建一个由较好性能的解析器并不容易,所以解析生成器很有用。Webkit使用两个知名的解析生成器——用于创建语法分析器的Flex创建解析器的Bison(你可能接触过Lex和Yacc)。Flex的输入是一个包含了符号定义的正则表达式,Bison的输入是用BNF格式表示的语法规则。

稍微总结:HTML 解析器按照 HTML DTD 文法定义进行 HTML 解析,可以认为解析生成的 DOM 树与解析树一一映射,DOM 树就是解析树。

1.1.4 解析算法

HTML 不能被一般的自顶向下或自底向上的解析器所解析,不能使用正则解析技术,浏览器为 HTML 定制了专属的解析器,原因是:

  1. 这门语言本身的宽容特性
  2. 浏览器对一些常见的非法html有容错机制
  3. 解析过程是往复的,通常源码不会在解析过程中发生改变,但在 HTML 中,脚本标签包含的 “document.write” 可能添加标签,这说明在解析过程中实际上修改了输入,所以不能使用正则解析技术

HTML5 规范中描述了这个解析算法,算法包括两个阶段——符号化及构建树。

符号化是词法分析的过程,将输入解析为符号,HTML 的符号包括开始标签、结束标签、属性名及属性值。符号识别器识别出符号后,将其传递给树构建器,并读取下一个字符,以识别下一个符号,直到处理完所有输入。

符号识别算法(The tokenization algorithm)

算法输出 HTML 符号,该算法用状态机表示。每次读取输入流中的一个或多个字符,并根据这些字符转移到下一个状态,当前的符号状态及构建树状态共同影响结果,这意味着,读取同样的字符,可能因为当前状态的不同,得到不同的结果以进入下一个正确的状态。

这个算法很复杂,这里用一个简单的例子来解释这个原理。

1
2
3
4
5
<html>
<body>
Hello world
</body>
</html>

初始状态为“Data State”,当遇到“<”字符,状态变为“Tag open state”,读取一个a-z的字符将产生一个开始标签符号,状态相应变为“Tag name state”,一直保持这个状态直到读取到“>”,每个字符都附加到这个符号名上,例子中创建的是一个html符号。

当读取到“>”,当前的符号就完成了,此时状态回到“Data state”,“\body>”重复这一处理过程。到这里,html和body标签都识别出来了。现在,回到“Data state”,读取“Hello world”中的字符“H”将创建并识别出一个字符符号,这里会为“Hello world”中的每个字符生成一个字符符号

这样直到遇到“</body>”中的“<”。现在,又回到了“Tag open state”,读取下一个字符“/”将创建一个闭合标签符号,并且状态转移到“Tag name state”,还是保持这一状态,直到遇到“>”。然后,产生一个新的标签符号并回到“Data state”。后面的“</html>”将和“</body>”一样处理。

image.png

简单总结:因为 HTML 符号包括开始标签、结束标签、属性名及属性值,所以解析每一个符号都需要三个状态来决定。状态机+符号化+树构建器。符号并不仅仅只是一个字符,可以是标签符号包括标签名、属性等,除标签符号外,为每一个内容字符创建一个字符符号(Data State)。

树的构建算法(Tree construction algorithm)

在树的构建阶段,将修改以Document为根的DOM树,将元素附加到树上。每个由符号识别器识别生成的节点将会被树构造器进行处理,规范中定义了每个符号相对应的Dom元素,对应的Dom元素将会被创建。这些元素除了会被添加到Dom树上,还将被添加到开放元素堆栈中。这个堆栈用来纠正嵌套的未匹配和未闭合标签,这个算法也是用状态机来描述,所有的状态采用插入模式。

首先是“initial mode”,接收到html符号后将转换为“before html”模式,在这个模式中对这个符号进行再处理。此时,创建了一个HTMLHtmlElement元素,并将其附加到根Document对象上。

状态此时变为“before head”,接收到body符号时,即使这里没有head符号,也将自动创建一个HTMLHeadElement元素并附加到树上。

现在,转到“in head”模式,然后是“after head”。到这里,body符号会被再次处理,将创建一个HTMLBodyElement并插入到树中,同时,转移到“in body”模式。

然后,接收到字符串“Hello world”的字符符号,第一个字符将导致创建并插入一个text节点,其他字符将附加到该节点。

接收到body结束符号时,转移到“after body”模式,接着接收到html结束符号,这个符号意味着转移到了“after after body”模式,当接收到文件结束符时,整个解析过程结束。

image.png

简单总结:树构建器根据符号创建对应的DOM元素,附加到以Document为根的DOM树上,并对一些常见的 HTML 错误进行处理。

1.1.5 解析结束时的处理(Action when the parsing is finished)

在这个阶段,浏览器将文档标记为可交互的,并开始解析处于延时模式中的脚本——这些脚本在文档解析后执行。文档状态将被设置为完成,同时触发一个 load 事件。

HTML5 规范中有符号化及构建树的完整算法:链接

1.1.6 浏览器容错(Browsers error tolerance)

详细可查看原文:前端必读:浏览器内部工作原理_知识库_博客园 (cnblogs.com)

1.2 CSS Parser

CSS 的加载并不会阻塞 DOM 的解析,但是会阻塞 DOM 的渲染。因为加载 CSS 时,可能会修改当前解析位置以后的 DOM 节点的样式,如果 CSS 加载不阻塞 DOM 树渲染,那么当 CSS 加载完之后,DOM 树可能又得重新重排 layout(回流reflow)或者重绘了,这就造成了一些没有必要的损耗。

不同于HTML,CSS属于上下文无关文法,可以用前面所描述的解析器来解析。CSS规范定义了CSS的词法及语法文法。可以参考词汇表和语法规则:前端必读:浏览器内部工作原理_知识库_博客园 (cnblogs.com)

每个符号都由正则表达式定义了词法文法(词汇表):

1
2
3
4
5
6
7
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”是识别器的缩写,相当于一个class名,“name”是一个元素id(用“#”引用)。

语法用BNF进行描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
selector
: simple_selector [ combinator selector | S+ [ combinator selector ] ]
;
class
: '.' IDENT
;
element_name
: IDENT | '*'
;

一个规则集合具有一个或是可选个数的多个选择器,这些选择器以逗号和空格(S表示空格)进行分隔。每个规则集合包含大括号及大括号中的一条或多条以分号隔开的声明。声明和选择器在后面进行定义。

1.2.1 Webkit CSS解析器(Webkit CSS parser)

Webkit使用Flex和Bison解析生成器从CSS语法文件中自动生成解析器。回忆一下解析器的介绍,Bison创建一个自底向上的解析器,Firefox使用自顶向下解析器。它们都是将每个css文件解析为样式表对象,每个对象包含css规则,css规则对象包含选择器和声明对象,以及其他一些符合css语法的对象。

用于创建语法分析器的Flex及创建解析器的Bison。Flex的输入是一个包含了符号定义的正则表达式,Bison的输入是用BNF格式表示的语法规则。

image.png

注意:WebKit使用的是自底向上的解析器

处理脚本及样式表的顺序(The order of processing scripts and style sheets)

脚本:web的模式是同步的,开发者希望解析到一个script标签时立即解析执行脚本,并阻塞文档的解析直到脚本执行完。如果脚本是外引的,则网络必须先请求到这个资源——这个过程也是同步的,会阻塞文档的解析直到资源被请求到。这个模式保持了很多年,并且在html4及html5中都特别指定了。开发者可以将脚本标识为defer,以使其不阻塞文档解析,并在文档解析结束后执行。HTML5增加了标记脚本为异步的选项,以使脚本的解析执行使用另一个线程。

async是在外部JS加载完成后,浏览器空闲时,Load事件触发前执行;而Defer是在JS加载完成后,整个文档解析完成后执行。 Defer更像是将<script>标签放在</body>之后的效果,但是它由于是异步加载JS文件,所以可以节省时间。简单来说就是async和defer都是开启新线程(http线程)请求JS文件,和解析DOM并行不冲突,async在请求完成后就开始执行JS文件,而defer则是在整个DOM解析完成后再执行JS文件。

预解析(Speculative parsing)

Webkit和Firefox都做了这个优化,当执行脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。需要注意的是,预解析并不改变Dom树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。

StyleSheets样式表采用另一种模式。理论上,既然样式表不改变Dom树,也就没有必要停下文档的解析等待它们,然而存在一个问题,脚本可能在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值,显然这将会导致很多问题,这看起来是个边缘情况,但确实很常见。Firefox在存在样式表还在加载和解析时阻塞所有的脚本,而Chrome只在当脚本试图访问某些可能被未加载的样式表所影响的特定的样式属性时才阻塞这些脚本。

简单总结:WebKit用Flex生成CSS语法规则,Bison生成CSS解析器。样式表不阻塞DOM解析,但是阻塞DOM渲染,FireFox在样式文件未加载完成前阻塞JS脚本,目的是防止样式文件未加载完成而JS脚本需要获取样式文件信息的错误,Chrome只阻塞那些访问未加载完成的样式文件的JS脚本,样式加载完成即不堵塞。

1.3 Render Tree

1.3.1 渲染树的定义

当Dom树构建完成时,浏览器开始构建另一棵树——渲染树。渲染树由元素显示序列中的可见元素组成,它是文档的可视化表示,构建这棵树是为了以正确的顺序绘制文档内容。

Firefox将渲染树中的元素称为frames,WebKit则用renderer或渲染对象来描述这些元素.一个渲染对象知道怎么布局及绘制自己及它的 children?

RenderObject是Webkit的渲染对象基类,它的定义如下:

1
2
3
4
5
6
7
8
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 盒模型相对应的矩形区域来表示,正如 CSS2 所描述的那样,它包含诸如宽、高和位置之类的几何信息。盒模型的类型受该节点相关的 display 样式属性的影响(参考样式计算章节)。下面的 WebKit 代码说明了如何根据 display 属性决定某个节点创建何种类型的渲染对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}

元素的类型也需要考虑,例如,表单控件和表格带有特殊的框架。

在Webkit中,如果一个元素想创建一个特殊的渲染对象,它需要重写“createRenderer”方法,使渲染对象指向不包含几何信息的样式对象。

简单总结:渲染树是可视节点的集合,即在页面真实渲染的元素集合,底层使用c++进行渲染,通过c++定义不同的对象实现不同类型节点的渲染

1.3.2 渲染树和DOM树的关系

渲染对象和Dom元素相对应,但这种对应关系不是一对一的,不可见的Dom元素不会被插入渲染树,如head元素,display属性为none的元素(渲染树中存在 visibility 属性为 hidden 或着 opacity 属性为0的元素)。

还有一些Dom元素对应几个可见对象,它们一般是具有复杂结构的元素,无法用一个矩形来描述。例如,select元素有三个渲染对象——一个显示区域、一个下拉列表及一个按钮。同样,当文本因为宽度不够而换行时,新行将作为额外的渲染元素被添加。另一个多个渲染对象的例子是不规范的 HTML结构,根据 CSS 规范,一个行内元素只能仅包含行内元素或仅包含块状元素,存在混合内容时,将会创建匿名的块状渲染对象包裹行内元素

image.png

1.3.3 创建树的流程(The flow of constructing the tree)

Firefox中,表述为一个监听Dom更新的监听器,将frame的创建委派给Frame Constructor,这个构建器计算样式(参看样式计算)并创建一个frame。

Webkit中,计算样式并生成渲染对象的过程称为 attachment,每个Dom节点有一个attach方法,attachment的过程是同步的,调用新节点的 attach 方法将节点插入到Dom树中。处理 html 和 body 标签将构建渲染树的根,这个根渲染对象对应被 css 规范称为 containing block 的元素——包含了其他所有块元素的顶级块元素。它的大小就是 viewport——浏览器窗口的显示区域,Firefox称它为 viewPortFrame,webkit称为 RenderView,这个就是文档 document 所指向的渲染对象,树中其他的部分都将作为一个插入的Dom节点被创建。

简单总结:不同的内核对于合并的过程有不同的称呼,WebKit中使用attachment,构建渲染树会扫描DOM树和CSSOM树,扫描DOM树发现HTML、BODY元素时,就会创建一个根渲染对象,称为containing block,大小就是viewport。

1.3.4 样式计算(Style Computation)

创建渲染树需要计算出每个渲染对象的可视属性,这可以通过计算每个元素的样式属性得到。

样式包括各种来源的样式表行内样式元素及html中的可视化属性(例如bgcolor),可视化属性转化为css样式属性。

样式表的来源包括浏览器默认样式表、页面作者以及用户提供的样式表(有些样式是浏览器用户提供的(浏览器允许用户定义喜欢的样式,例如,在Firefox中可以通过在Firefox Profile目录下放置样式表实现))。

计算样式的一些困难:

  1. 样式数据是非常大的结构,保存大量的样式属性会带来内存问题。
  2. 如果不进行优化,找到每个元素匹配的规则会导致性能问题,为每个元素查找匹配的规则都需要遍历整个规则表,这个过程有很大的工作量。选择符可能有复杂的结构,匹配过程如果沿着一条开始看似正确,后来却被证明是无用的路径,则必须去尝试另一条路径。例如,复杂选择符 div div div div{…} 这意味着规则应用到三个div的后代div元素,选择树某一条特定的路径去检查,这可能需要遍历节点树,最后却发现它只是两个div的后代,并不使用该规则,然后则需要沿着另一条路径去尝试
  3. 应用规则涉及非常复杂的级联,它们定义了规则的层次

浏览器如何处理这些问题?

共享样式数据(Sharing style data)

WebKit节点引用样式对象(渲染样式),某些情况下,这些对象可以被节点间共享,这些节点需要是兄弟或是表兄弟节点,并且:

  1. 这些元素必须处于相同的鼠标状态(比如不能一个处于hover,而另一个不是)
  2. 不能有元素具有id
  3. 标签名必须匹配
  4. class属性必须匹配
  5. 对应的属性必须相同
  6. 链接状态必须匹配
  7. 焦点状态必须匹配
  8. 不能有元素被属性选择器影响
  9. 元素不能有行内样式属性
  10. 不能有生效的兄弟选择器,webcore在任何兄弟选择器相遇时只是简单的抛出一个全局转换,并且在它们显示时使整个文档的样式共享失效,这些包括+选择器和类似:first-child和:last-child这样的选择器。

Firefox规则树(Firefox rule tree)

Firefox用两个树用来简化样式计算-规则树和样式上下文树,WebKit也有样式对象,但它们并没有存储在类似样式上下文树这样的树中,只是由Dom节点指向其相关的样式。

image.png

具体细节可以阅读原文:浏览器内部工作原理_知识库_博客园 (cnblogs.com)

Webkit中,并没有规则树,匹配的声明会被遍历四次,先是应用非important的高优先级属性(之所以先应用这些属性,是因为其他的依赖于它们-比如display),其次是高优先级important的,接着是一般优先级非important的,最后是一般优先级important的规则。这样,出现多次的属性将被按照正确的级联顺序进行处理,最后一个生效。

1.4 Layout

当渲染对象被创建并添加到树中,它们仅具有对应的布局规则,并没有实际的位置和大小,计算这些值的过程称为 layout 或 reflow。

Html使用基于流的布局模型,意味着大部分时间,可以以单一的途径进行几何计算。流中靠后的元素并不会影响前面元素的几何特性,所以布局可以在文档中从右向左自上而下的进行。也存在一些例外,比如 html tables。

坐标系统相对于根frame,使用 top 和 left 坐标。布局是一个递归的过程,由根渲染对象开始,它对应 html 文档document元素,布局继续递归的通过一些或所有的frame层级,为每个需要几何信息的渲染对象进行计算。根渲染对象的位置是 0,0,它的大小是viewport-浏览器窗口的可见部分。所有的渲染对象都有一个 layout 或 reflow 方法,每个渲染对象调用需要布局的 children 的 layout 方法。

简单总结:通过父元素掉调用需要布局的 children 的 layout 方法,如何明确元素需要布局,则需要使用到 Dirty bit 系统

1.4.1 Dirty bit系统

为了不因为每个小变化都全部重新布局,浏览器使用一个 dirty bit 系统,一个渲染对象发生了变化或是被添加了,就标记它及它的children为dirty——需要layout。

dirty bit 设置元素存在两个标识——dirty及children are dirty,children are dirty标识说明即使这个渲染对象本身的属性没变化,它也至少有一个child需要layout。

1.4.2 全局和增量layout

当layout在整棵渲染树触发时,称为全局layout,这可能在下面这些情况下发生:

  1. 一个全局的样式改变影响所有的渲染对象,比如字号的改变。
  2. 窗口resize。

layout也可以是增量的,这样只有标志为dirty的渲染对象会重新布局(也将导致一些额外的布局)。增量layout会在渲染对象dirty时异步触发,例如,当网络接收到新的内容并添加到DOM树后,新的渲染对象会添加到渲染树中,即在初次渲染时,是一个增量layout的过程。

image.png

异步和同步layout

增量layout的过程是异步的,Firefox为增量 layout 生成了 reflow 队列,以及一个调度执行这些批处理命令。WebKit也有一个计时器用来执行增量layout-遍历树,为dirty状态的渲染对象重新布局。另外,当脚本请求样式信息时,例如“offsetHeight”,会同步的触发增量布局。

全局的layout一般都是同步触发。有些时候,layout会被作为一个初始layout之后的回调,比如滑动条的滑动。

优化

当一个layout因为resize或是渲染位置改变(并不是大小改变)而触发时,渲染对象的大小将会从缓存中读取,而不会重新计算。

一般情况下,如果只有子树发生改变,则layout并不从根开始。这种情况发生在,变化发生在元素自身并且不影响它周围元素,例如,将文本插入文本域(否则,每次击键都将触发从根开始的重排)。

1.4.3 layout过程

layout一般有下面这几个部分:

  1. parent渲染对象决定它的宽度

  2. parent渲染对象读取chilidren,并:

    • 放置child渲染对象(设置它的x和y)
    • 在需要时(它们当前为dirty或是处于全局layout或者其他原因)调用child渲染对象的layout,这将计算child的高度
    • parent渲染对象使用child渲染对象的累积高度,以及margin和padding的高度来设置自己的高度-这将被parent渲染对象的parent使用
    • 将dirty标识设置为false

Firefox使用一个“state”对象(nsHTMLReflowState)做为参数去布局(firefox称为reflow),state包含parent的宽度及其他内容。

Firefox布局的输出是一个“metrics”对象(nsHTMLReflowMetrics)。它包括渲染对象计算出的高度。

宽度计算

渲染对象的宽度使用容器的宽度、渲染对象样式中的宽度及margin、border进行计算。例如,下面这个div的宽度:

  

webkit中宽度的计算过程是(RenderBox类的 calcWidth 方法):

  • 容器的宽度是容器的可用宽度和0中的最大值,这里的可用宽度为:contentWidth=clientWidth()-paddingLeft()-paddingRight(),clientWidth和clientHeight代表一个对象内部的不包括border和滑动条的大小
  • 元素的宽度指样式属性width的值,它可以通过计算容器的百分比得到一个绝对值
  • 加上水平方向上的border和padding

到这里是最佳宽度的计算过程,现在计算宽度的最大值和最小值,如果最佳宽度大于最大宽度则使用最大宽度,如果小于最小宽度则使用最小宽度。最后缓存这个值,当需要layout但宽度未改变时使用。

Line breaking

当一个渲染对象在布局过程中需要折行时,则暂停并告诉它的parent它需要折行,parent将创建额外的渲染对象并调用它们的layout。

1.5 绘制(Painting)

  绘制阶段,遍历渲染树并调用渲染对象的paint方法将它们的内容显示在屏幕上,绘制使用UI基础组件,这在UI的章节有更多的介绍。

1.5.1 全局和增量

和布局一样,绘制也可以是全局的——绘制完整的树,或增量的。在增量的绘制过程中,一些渲染对象以不影响整棵树的方式改变,改变的渲染对象使其在屏幕上的矩形区域失效,这将导致操作系统将其看作dirty区域,并产生一个paint事件,操作系统很巧妙的处理这个过程,并将多个区域合并为一个,提高性能。Chrome中,这个过程更复杂些,因为渲染对象在不同的进程中,而不是在主进程中。Chrome在一定程度上模拟操作系统的行为,表现为监听事件并派发消息给渲染根,在树中查找到相关的渲染对象,重绘这个对象(往往还包括它的children)。

1.5.2 绘制顺序

css2定义了绘制过程的顺序——http://www.w3.org/TR/CSS21/zindex.html。这个就是元素压入堆栈的顺序,这个顺序影响着绘制,堆栈从后向前进行绘制

一个块渲染对象的堆栈顺序是:

  1. 背景色
  2. 背景图
  3. border
  4. children
  5. outline

1.5.3 Firefox显示列表

Firefox读取渲染树并为绘制的矩形创建一个显示列表,该列表以正确的绘制顺序包含这个矩形相关的渲染对象(背景、图片、样式属性等以正确的顺序排序)。用这样的方法,可以使重绘时只需查找一次树,而不需要多次查找——绘制所有的背景、所有的图片、所有的border等等。

Firefox优化了这个过程,它不添加会被隐藏的元素,比如元素完全在其他不透明元素下面。

1.5.4 WebKit矩形存储

重绘前,WebKit将旧的矩形保存为位图,然后只绘制新旧矩形的差集

1.5.5 动态变化

浏览器总是试着以最小的动作响应一个变化,所以一个元素颜色的变化将只导致该元素的重绘,元素位置的变化将大致元素的布局和重绘,添加一个Dom节点,也会导致这个元素的布局和重绘。一些主要的变化,比如增加html元素的字号,将会导致缓存失效,从而引起整体的布局和重绘。

1.6 渲染引擎的线程

渲染引擎是单线程的,除了网络操作以外,几乎所有的事情都在单一的线程中处理,在Firefox和Safari中,这是浏览器的主线程,Chrome中这是tab的主线程(Chrome一个Tab一个进程,一个进程中含有一个主线程,负责大部分的代码执行)。

网络操作由几个并行线程执行,并行连接的个数是受限的(通常是2-6个)。

1.7 事件循环

浏览器主线程是一个事件循环,它被设计为无限循环以保持执行过程的可用,等待事件(例如layout和paint事件)并执行它们,注意并不是无限制的轮询,而是一个带有阻塞的轮询。下面是Firefox的主要事件循环代码

1
2
while (!mExiting)
NS_ProcessNextEvent(thread);

1.8 CSS2可视模型(CSS2 visual module)

1.8.1 画布The Canvas

根据CSS2规范,术语canvas用来描述格式化的结构所渲染的空间——浏览器绘制内容的地方。画布对每个维度空间都是无限大的,但浏览器基于viewport的大小选择了一个初始宽度。根据http://www.w3.org/TR/CSS2/zindex.html的定义,画布如果是包含在其他画布内则是透明的,否则浏览器会指定一个颜色。

1.8.2 CSS盒模型

CSS盒模型描述了矩形盒,这些矩形盒是为文档树中的元素生成的,并根据可视的格式化模型进行布局。每个box包括内容区域(如图片、文本等)及可选的四周padding、border和margin区域。

image.png

每个节点生成0-n个这样的box。所有的元素都有一个display属性,用来决定它们生成box的类型,例如:

  • block -生成块状box
  • inline -生成一个或多个行内box
  • none -不生成box

默认的是inline,但浏览器样式表设置了其他默认值,例如,div元素默认为block。可以访问http://www.w3.org/TR/CSS2/sample.html查看更多的默认样式表示例。

1.8.3 定位策略Position scheme

这里有三种策略:

  1. normal -对象根据它在文档的中位置定位,这意味着它在渲染树和在Dom树中位置一致,并根据它的盒模型和大小进行布局。
  2. float -对象先像普通流一样布局,然后尽可能的向左或是向右移动。
  3. absolute -对象在渲染树中的位置和Dom树中位置无关

static 和 relative 是normal, float属于float,absolute和fixed属于absolute。

在static定位中,不定义位置而使用默认的位置。其他策略中,作者指定位置——top、bottom、left、right。

1.8.4 Box布局

Box布局的方式由这几项决定:box的类型、box的大小、定位策略及扩展信息(比如图片大小和屏幕尺寸)。

Box类型

  • Block box:构成一个块,即在浏览器窗口上有自己的矩形
  • Inline box:并没有自己的块状区域,但包含在一个块状区域内

区分

  • block一个挨着一个垂直格式化,inline则在水平方向上格式化。

image.png

  • Inline盒模型放置在行内或是line box中,每行至少和最高的box一样高,当box以baseline对齐时——即一个元素的底部和另一个box上除底部以外的某点对齐,行高可以比最高的box高。当容器宽度不够时,行内元素将被放到多行中,这在一个p元素中经常发生。

image.png

1.8.5 定位Position

Relative

相对定位——先按照一般的定位,然后按所要求的差值移动。

image.png

Floats

一个浮动的box移动到一行的最左边或是最右边,其余的box围绕在它周围。下面这段html:

1
2
3
<p>
<img src="images/image.gif" width="100" height="100">Lorem ipsum dolor sit amet, consectetuer...
</p>

image.png

Absolute和Fixed

这种情况下的布局完全不顾普通的文档流元素不属于文档流的一部分,大小取决于容器。Fixed时,容器为viewport(可视区域)。

image.png

Layered representation

这个由CSS属性中的z-index指定,表示盒模型的第三个大小,即在z轴上的位置。Box分发到堆栈中(称为堆栈上下文),每个堆栈中靠后的元素将被较早绘制,栈顶靠前的元素离用户最近,当发生交叠时,将隐藏靠后的元素。堆栈根据z-index属性排序,拥有z-index属性的box形成了一个局部堆栈,viewport有外部堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
<STYLE type="text/css">
div {
position: absolute;
left: 2in;
top: 2in;
}
</STYLE>
<p>
<div>
</div>
<div>
</div>
</p>

image.png

虽然绿色div排在红色div后面,可能在正常流中也已经被绘制在后面,但z-index有更高优先级,所以在根box的堆栈中更靠前。