# 高级基础架构

主要组件

  1. 用户界面
  2. 浏览器引擎
  3. 渲染引擎
  4. 网络
  5. 界面后端
  6. JavaScript 解释器
  7. 数据存储

浏览器架构

多进程:每个标签页都是一个独立的进程,每个标签页一个渲染引擎实例

# 渲染引擎

渲染引擎默认可以渲染 HTML 和图片,也可以搭配插件渲染 PDF

WebKit:Safari
Blink(WebKit 另一个分支):Chrome
Gecko:Firefox

# 主要流程

基本流程:

  1. 解析 HTML 文档构建 DOM tree
  2. 渲染 DOM tree
  3. 布局 Render tree
  4. 绘制 Render tree

引擎开始解析 HTML 文档,同时构建 DOM tree,解析外部 CSS 文件和样式元素中的样式数据。并结合样式信息和 DOM tree 一起构建 Render tree。
渲染树包含具有颜色和尺寸等视觉属性的矩形
渲染树构建完毕会经过 布局 阶段,为每个节点计算屏幕上显示的坐标。 下一个阶段经过 绘制 阶段,遍历渲染树,并调用 界面后端在屏幕上显示。

上述是一个渐进的过程,会边解析边渲染,解析和渲染可以交替进行。

在 webkit 中,“渲染树” 由 “渲染对象” 组成

渲染流程

# 解析 - 常规

解析文档 => 将文档转换为代码可以使用的结构,通常是代表文档结构的节点树(一般称为 “语法树 “)

一个 ” 3 + 5 - 2“ 的样例
词法法则:

1
2
3
INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -

语法结构树如下:

表达树

# 语法

解析基于文档遵循的语法规则:文档所用的语言或格式。
可以解析的每种格式都必须具有由 词汇语法规则 组成的确定性语法。这种语言称为无上下文语法

# 解析器 - 词法分析器

两个过程:

  1. 词法分析
  2. 语法分析

词法分析:将输入分解为称为标记的更小部分。标记是语言中具有特殊含义的单词。 令牌(token):它包含该语言字典中显示的所有字词

语法分析:将标记重新组合到语法树中。

流程:
词法分析流程

# 翻译

解析通常用于翻译:将输入文档转换为其他格式,比如转成机器语言

翻译流程

# 解析器类型

这一部分详细的可以去学习一下 CS143

  1. 自上而下解析器
  2. 自下而上解析器

自上而下解析器:从顶部开始,逐步向下,尝试匹配输入的标记。
自下而上解析器:从底部开始,逐步向上,尝试匹配输入的标记。 (移位 - 规约解析起)

# 自动解析生成器

Flex 和 Bison 同样可以学习一下 CS143 课程 里面有用到
WebKit 使用 Flex(词法) 和 Bison(语法) 自动生成解析器

# 解析器类型

  1. 自上而下解析器
  2. 自下而上解析器

# HTML 解析器

HTML 解析器的任务是将 HTML 标记解析为解析树。

# HTML 语法

HTML 并不是一种上下文无关语法,定义 HTML 的正式格式是 DTD(文档类型定义)

# DOM

输出树(“解析树”)是 DOM 元素和属性节点的树。 DOM 是文档对象模型的简称。它是 HTML 文档的对象呈现,也是 HTML 元素与外界(例如 JavaScript)的接口。
Document 是 DOM 树的根节点。

例如:

1
2
3
4
5
6
7
8
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>

对应如下 DOM 树
DOM树

# 令牌化算法

令牌化是 HTML 解析的基础,它的目标是将原始的 HTML 字符串转换成一系列的令牌。这个过程通常采用状态机模型,它会根据当前字符和状态来决定如何生成令牌。

下面是状态机示例:

状态机

# 树构建算法

创建解析器时,系统会创建 Document 对象。在树构建阶段,系统会修改根目录中包含文档的 DOM 树,并向其中添加元素。分词器发出的每个节点都将由树构造函数处理。对于每个令牌,规范会定义哪些 DOM 元素与其相关,以及将为此令牌创建哪些 DOM 元素。该元素会添加到 DOM 树和打开的元素堆栈中。此堆栈用于更正嵌套不匹配和未闭合标记。 该算法还可描述为状态机。这些状态称为 “插入模式”。

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

理解一下这个算法:

  1. 初始模式
  2. 遇到 html 令牌,切换 html 前模式 这将创建 HTMLHtmlElement 元素,并附着到 Document 上
  3. 状态更改为 head 前,同时收到 body 令牌,创建并插入 HTMLHeadElement 元素
  4. 状态更改为 head 后, 处理正文部分
  5. 创建并插入 HTMLBodyElement 元素, 进入 body 模式
  6. 遇到文本令牌,创建并插入 Text 节点
  7. 状态改为 body 后
  8. 状态改为 html 后,结束解析

# 解析完成后的操作

浏览器会将文档标记为交互式,并开始解析处于 “延迟” 模式的脚本:这些脚本应在文档解析完毕后执行。然后,文档状态将设为 “complete”,并触发 “load” 事件。

# CSS 解析

CSS 是上下文无关语法

# WebKit CSS 解析起

WebKit 使用 Flex 和 Bison 解析器生成器从 CSS 语法文件自动创建解析器。如解析器简介中所述,Bison 会创建自底向上的移位 - 规约解析器。Firefox 使用手动编写的顶向下解析器。在这两种情况下,每个 CSS 文件都会解析为 StyleSheet 对象。每个对象都包含 CSS 规则。CSS 规则对象包含选择器和声明对象,以及与 CSS 语法对应的其他对象。

CSS解析

# 脚本和样式表的处理顺序

# script 脚本

Web 模型是同步的,遇到 <script> 标记会立即解析和执行脚本,如果是外部资源脚本,同样会暂停解析并等待资源获取及执行完毕

作者可以向脚本添加 “defer” 属性,在这种情况下,脚本不会停止文档解析,而是会在文档解析完毕后执行。HTML5 添加了一个选项 ”async“,用于将脚本标记为异步,以便由其他线程解析和执行。

# 推测解析

WebKit 和 Firefox 都会执行此优化。在执行脚本时,另一个线程会解析文档的其余部分,找出需要从网络加载的其他资源,并加载这些资源。这样,系统就可以在并行连接上加载资源,从而提高整体速度。注意:推测性解析器仅解析对外部资源(例如外部脚本、样式表和图片)的引用:它不会修改 DOM 树,而是将此任务交给主解析器。

# 样式表

另一方面,样式表采用的是不同的模型。 从概念上讲,由于样式表不会更改 DOM 树,因此没有理由等待它们并停止文档解析。不过,在文档解析阶段,脚本会请求样式信息,这会导致一个问题。如果样式尚未加载和解析,脚本将获得错误的答案,这显然会导致很多问题。这似乎是一个极端情况,但却很常见。如果有样式表仍在加载和解析中,Firefox 会屏蔽所有脚本。 只有当脚本尝试访问可能受未加载样式表影响的特定样式属性时,WebKit 才会阻止脚本。

# 渲染树的构建

在构建 DOM 树时,浏览器会构建 渲染树。此树状结构显示了视觉元素的显示顺序,支持正确的顺序绘制内容。
渲染程序(渲染树节点)知道如何绘制布局和绘制自身及其子项
WebKit 的 RenderObject 类(即渲染程序的基类)具有以下定义:

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 属性确定应为 DOM 节点创建哪种类型的渲染程序的 WebKit 代码:

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

# 渲染树与 DOM 树

  1. 渲染程序与 DOM 元素相对应,但不是一对一。非可视 DOM 不会被插入到渲染树(head, display 为 none 的元素)

  2. 有些 DOM 元素对应于多个视觉对象。这些通常是结构复杂的元素,无法用单个矩形来描述。例如,“select” 元素有三个渲染程序:一个用于显示区域,一个用于下拉列表框,一个用于按钮。此外,如果文本因宽度不足以显示一行而被拆分为多行,则系统会将新行添加为额外的渲染程序。

  3. 某些渲染对象与 DOM 节点相对应,但不在树中的同一位置。浮动元素和绝对定位元素不在流中,放置在树的其他部分,并映射到真实框架

# 布局

# 脏位系统

为了避免对每项细微更改都进行完整布局,浏览器使用 “脏位” 系统。更改或添加的渲染程序会将自身及其子项标记为 “脏”:需要布局。

# 全局布局和增量布局

布局可在整个渲染树上触发 - 这是 “全局” 布局。造成这种情况的原因可能是:

  1. 会影响所有渲染程序的全局样式更改,例如字号更改。
  2. 由于屏幕大小调整

布局可以是增量布局,只有脏渲染程序才会进行布局(这可能会导致一些损坏,需要额外的布局)。
例如:

  1. 当额外内容从网络传入并添加到 DOM 树后,将新的渲染程序附加到渲染树中时。

# 异步布局和同步布局

增量布局是异步完成的。 webkit 有一个用于执行增量布局的计时器,它会遍历树并布局 “脏” 渲染程序。

请求样式信息(例如 “offsetHeight”)的脚本可以同步触发增量布局。

全局布局通常会同步触发。

# 布局流程

  1. 父级渲染程序确定自己的宽度。
  2. 父级遍历子级,并执行以下操作:
  3. 放置子渲染程序(设置其 x 和 y)。
  4. 在需要时调用子布局(如果它们不干净、处于全局布局或出于其他原因),以计算子项的高度。
  5. 父级使用子项的累计高度以及边距和内边距的高度来设置自己的高度,父级渲染程序的父级将使用此高度。
  6. 脏标记置为 false

# 绘画

在绘制阶段,系统会遍历渲染树并调用渲染程序的 “paint ()” 方法,以便在屏幕上显示内容。绘制使用界面基础架构组件。

# WebKit 矩形存储

在重新绘制之前,WebKit 会将旧矩形保存为位图。然后,它只会绘制新矩形与旧矩形之间的差异。

# 渲染引擎的线程

渲染引擎是单线程的。除了网络操作之外,几乎所有操作都在单个线程中进行。在 Firefox 和 Safari 中,这是浏览器的主线程。在 Chrome 中,它是标签页进程的主线程。

网络操作可以由多个并行线程执行。并行连接数有限(通常为 2 到 6 个连接)。

# 事件循环

浏览器主线程是一个事件循环。 这是一个无限循环,可让进程保持活跃状态。它会等待事件(例如布局和绘制事件),并对其进行处理。以下是主事件循环的 Firefox 代码:

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