Web 浏览器概述
历史
JavaScript 最初是为了 Web 浏览器创立的。
下图是运行在 Web 浏览器宿主环境中的一个鸟瞰
作为根对象的 window 有两个作用:
- 作为 JavaScript 代码的全局(global)对象。
- 代表浏览器的对象,提供方法来操作浏览器。
主要组件
浏览器的主要功能是展示用户选择的 web 资源,从服务器请求并在浏览器窗口显示。资源的位置由用户使用 URI(统一资源标示符)指定。
组件包括:
-
用户界面 - 浏览器的用户界面没有正式的规范,是多年来优秀实践以及相互模仿的结果,包括:
- 用来输入 URI 的地址栏
- 前进、后退、刷新、停止、主页、书签等按钮
- 状态栏和工具栏
- 显示请求内容的主窗口
- 浏览器引擎 - 用于查询和操作渲染引擎的接口。
- 渲染引擎 - 负责显示请求的内容。例如请求 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
基本流程解析 HTML 文档,生成 DOM 树,同时解析样式数据,这些和 HTML 视觉结合的样式信息,将用于创建渲染树。渲染树包含多个带有视觉属性(如颜色和尺寸)的矩形,这些矩形的排列顺序就是它们将在屏幕上显示的顺序。渲染树构建之后,开始“布局”处理阶段,为每个节点分配一个应出现在屏幕上的确切坐标。下一个阶段是绘制 - 渲染引擎会遍历渲染树,由用户界面后端层将每个节点绘制出来。
这是一个渐进的过程。渲染引擎会尽快将内容显示在屏幕上,不必等整个 HTML 文档解析完,就开始构建渲染树和设置布局。在不断接收和处理来自网络的其余内容的同时,渲染引擎会将部分内容解析并显示出来。
- 网络 - 用于网络调用,比如 HTTP 请求。
- 用户界面后端(UI Backend) - 用于绘制基本的窗口组件,比如组合框和窗口。暴露与平台无关的通用接口,底层使用操作系统的用户界面方法。
- JavaScript 解释器 - 用于解析和执行 JavaScript 代码。
- 数据存储 - 浏览器需要在硬盘上保存各种数据,例如 Cookie。HTML5 定义了完整的“网络数据库”。
渲染引擎采用了单线程,几乎所有操作(除了网络操作)都是在单线程中进行的。在 Firefox 和 Safari 中,该线程就是浏览器的主线程。而在 Chrome 浏览器中,该线程是标签进程的主线程。网络操作可由多个并行线程执行,并行连接数是有限的。
DOM 文档对象模型(Document Object Model)
解析器输出的解析树由 DOM 元素和属性节点构成。DOM 是 HTML 文档的对象表示,同时也是外部内容(例如 JavaScript)与 HTML 元素之间的接口。解析树的根节点是 document 对象。
在没规范时,各浏览器只实现自己想要的。为了避免混乱,大家达成一致制定 DOM 规范,第一版叫 “DOM Level 1”,依次迭代。
DOM 规范解释了一个文档的结构和定义了一些属性和方法去操作它,由两个工作组共同维护:
两个工作组不是完全一致的,但差异很小。每个浏览器实现时也还是会根据自身情况加些不同的方法或属性。
具体的属性和方法,推荐 MDN DOM,更深入学习,读规范更好。
BOM 浏览器对象模型(Browser Object Model)
BOM 是浏览器给文档提供的一些附加对象,例如 navigator 和 location 对象。
alert、confirm、setTimeout 等函数也属于 BOM,它们和文档没直接关联,单纯为了浏览器和用户交互。
BOM 本身也是广义的 HTML 规范一部分。
主流浏览器及内核(Browser Kernel)
- IE(Internet Explorer) 浏览器,Microsoft 1994年发布,1997年首次采用 Trident 内核;2015年发布全新的浏览器 Edge,基于 EdgeHTML 内核,2020年基于 Chromium 内核。
- Firefox 浏览器,Mozilla 2004年发布,基于 Gecko 内核,Gecko 特点开放、跨平台,其中 JavaScript 引擎是 SpiderMonkey。
- Safari 浏览器,Apple 2004年发布,基于 KHTML 的 WebKit 内核,其中 JavaScript 引擎是 JavaScriptCore,代号 Nitro。
- Chrome 浏览器,Google 2008年发布,渲染引擎基于 Apple 的 WebKit,2013年起,使用基于 WebKit 分支开发的 Blink 做渲染引擎,JavaScript 引擎是自身的 V8。早在2006年,为发展 Chrome,Google 启动 Chromium 浏览器开源项目,新功能一般会先在 Chromium 上实现,待验证后应用在 Chrome 上。
国产浏览器一般基于以上主流浏览器的单内核或双内核开发。
WebKit 主流程
Gecko 主流程
解析(Parsing)综述
解析文档是指将文档转化成为可让代码理解和使用的结构,通常是代表了文档结构的节点树,即解析树或者语法树。
解析是基于文档所遵循的语法(syntax)规则(文档所用的语言或格式)。每个可以解析的格式必须有对应的确定文法(grammar)(由词汇 vocabulary 和语法规则构成),这称为上下文无关(content free)的文法。
// 词汇通常用正则表达式表示,示例
// 整数
INTEGER : 0|[1-9][0-9]*
// 语法通常使用 BNF 格式来定义,示例:
// 表达式
expression := term operation term
// 操作符
operation := PLUS | MINUS
// 项
term := INTEGER | expression
解析过程可以分成两个子过程:词法分析(lexical analysis)和语法分析。词法分析是通过词法分析器(也称标记生成器)将输入内容分解成有效标记(tokens)的过程;语法分析是应用语言的语法规则的过程。
解析是一个迭代的过程,解析器向词法分析器请求一个新标记,并尝试与语法规则进行匹配,如果匹配,解析器会将一个对应于该标记的节点添加到解析树中,然后请求下一个标记。如果没有匹配,解析器就会将标记存储到内部,并继续请求标记,直至找到可与所有内部存储的标记匹配的规则,如果找不到任何匹配规则,解析器就会引发一个异常,意味文档无效,包含语法错误。
解析器有两种类型:自上而下解析器(top down parsers,从语法的高层结构出发,尝试匹配)和自下而上解析器(bottom up parsers,从低层规则出发,将输入内容逐步转化为语法规则,直至满足高层规则)。
人工创建优化的解析器并不是一件容易的事情,通常借用工具,Webkit 使用了两种解析器自动生成器:用于创建词法分析器的 Flex、用于创建解析器的 Bison,类似 Lex&YACC)。
解析树通常不是最终产品,还需经过翻译(translation),换成其他格式。例如编译,编译器(compiler)首先将源代码解析成解析树,然后将解析树翻译成机器代码。
HTML 解析器
HTML 解析器的任务是将 HTML 标记解析成解析树。
和 XML 严格的语法不同,HTML 的处理更为“宽容”:
- 语言本身的宽容,例如允许省略某些隐式添加的标记,允许省略一些起始或者结束标记。
- 浏览器历来对一些常见的无效 HTML 用法采取包容态度,不同浏览器的错误处理机制相当一致,这是浏览器在多年发展中的产物。
- 解析过程需要不断地反复(reentrant),HTML 解析过程中可以添加额外的标记,实际上修改了原始输入内容。
这是 HTML 如此流行的原因:包容错误,方便 web 编写。但也使得很难用上下文无关的语法来定义,导致常规解析器都不适用于 HTML(它们可以用于解析 CSS 和 JavaScript)。目前定义 HTML 的正规格式是 DTD(Document Type Definition,文档类型定义),不是上下文无关的语法。
浏览器需要创建自定义的解析器来解析 HTML,其中的解析算法在HTML5 规范中有详细描述,http://www.w3.org/TR/html5/syntax.html#html-parser,包括标记化算法和树构建算法,简化过程如下:。
在创建解析器的同时,也会创建 document 对象。在树构建阶段,以 document 为根节点的 DOM 树也会不断进行修改,为不断接收的标记创建对应的 DOM 元素。这些元素不仅会添加到 DOM 树中,还会添加到开放元素的堆栈中,此堆栈用于纠正嵌套错误和处理未关闭的标记。
<html>
<body>
Hello world
</body>
</html>
解析结束后的,浏览器会将文档标记为交互状态(interactive),并开始解析那些处于“deferred”模式的脚本——应在文档解析完成后才执行的脚本。然后,文档状态将设置为“完成”(complete),并触发“load”事件。
CSS 解析
CSS 是上下文无关的语法,可以用各种解析器进行解析。词汇和语法,见https://www.w3.org/TR/CSS2/grammar.html。
语法基于 BNF 格式定义,一个规则集(ruleset)就是一个选择器(selector),或者由逗号和空格分隔的多个选择器。规则集包含了大括号,以及一个或多个由分号分隔的声明。
Webkit CSS 解析器使用 Flex 和 Bison 解析器生成器,通过 CSS 语法文件自动创建。Gecko 使用人工编写的自上而下的解析器。这两种解析器都会将 CSS 文件解析成 StyleSheet 对象,且每个对象都包含 CSS 规则。CSS 规则对象则包含选择器和声明对象,以及其他与 CSS 语法对应的对象。
处理脚本和样式表的顺序
web 脚本模型是同步的,web 作者希望解析器遇到 <script> 标记时立即解析并执行脚本。文档的解析将停止,直到脚本执行完毕。如果脚本是外部的,那么解析过程会停止,直到从网络同步抓取资源完成后再继续。HTML5 增加了一个选项,将脚本标记为“defer”,这样它就不会停止文档解析,而是等到解析结束才执行。
Webkit 和 Gecko 都进行预解析(Speculative parsing)。在执行脚本时,其他线程会解析文档的其余部分,找出需要通过网络加载的其他资源,并加载。资源并行加载,提高了总体速度。注意,预解析器不会修改 DOM 树,只会解析外部资源(例如外部脚本、样式表和图片)的引用。
样式表有着不同的模型。理论上,应用样式表不会更改 DOM 树,因此似乎没有必要等待样式表并停止文档解析。但这有一个问题,脚本在文档解析阶段会请求样式信息,如果当时还没有加载和解析样式,脚本就会取到错误的结果,这样显然会产生很多问题。Gecko 在样式表加载和解析的过程中,会禁止所有脚本。Webkit 仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。
渲染树
在 DOM 树构建的同时,浏览器还会构建另一个树结构:渲染树。这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。Gecko 将渲染树中的元素称为“框架”。Webkit 使用的术语是渲染器或渲染对象。渲染器知道如何布局并将自身及其子元素绘制出来。 Webkit 的 RenderObject 类是所有呈现器的基类。
每一个渲染器都代表了一个矩形的区域,通常对应于相关节点的 CSS 框。包含诸如宽度、高度和位置等几何信息。框的类型会受到与节点相关的“display”样式属性的影响。
渲染器是和 DOM 元素相对应的,但并非一一对应。
- 非可视化的 DOM 元素不会插入渲染树中,例如“head”元素、display 属性值为“none”元素。
- 复杂结构的 DOM 元素会对应多个可视化对象,例如“select”元素。
- 浮动和绝对定位的元素,处于正常的流之外,在渲染树中位置与 DOM 树中不同,并映射到实际的 frame,而放在原位的是占位 frame。
在 Gecko 中,会针对 DOM 更新注册展示层,作为侦听器。展示层将框架创建工作委托给 FrameConstructor。在 Webkit 中,解析样式和创建渲染器的过程称为“附加”(attachment)。每个 DOM 节点都有一个“attach”方法。附加是同步进行的,将节点插入 DOM 树需要调用新的节点“attach”方法。
处理 html 和 body 标记就会构建渲染树根节点。这个根节点渲染对象对应于 CSS 规范中所说的最上层容器 block,包含了其他所有 block。它的尺寸就是视口(viewport),即浏览器窗口显示区域的尺寸。Gecko 称之为 ViewPortFrame,Webkit 称之为 RenderView。
样式计算存在以下难点:
- 样式数据是一个超大的结构,存储了许多样式属性,这可能造成内存问题。
- 如果不进行优化,为每一个元素遍历整个规则列表来寻找匹配规则,是一项繁重工程,会造成性能问题。复杂结构的选择器,可能导致许多无效的尝试匹配。
- 应用规则涉及到相当复杂的层叠规则。
浏览器解决办法:
- 共享样式数据:Webkit 节点会引用样式对象 (RenderStyle)。这些对象在某些情况下可以由不同节点共享,这些节点是同级关系。
- 规则树(rule tree):Gecko 还采用了另外两种树:规则树和样式上下文树(style context tree)。规则树包含了所有已知规则匹配的路径,路径中的底层节点拥有较高的优先级。规则的存储是延迟进行的。规则树不会在开始的时候就为所有的节点进行计算,而是只有当某个节点样式需要进行计算时,才会向规则树添加计算的路径。
- 对规则进行处理以简化匹配。样式表解析完毕后,根据选择器将 CSS 规则添加到相应哈希表中。不同选择器类型使用不同哈希表。
- 样式表层叠顺序(优先级从低到高):浏览器声明 Browser declarations > 用户普通声明 User normal declarations -> 作者普通声明 Author normal declarations -> 作者重要声明 Author important declarations-> 用户重要声明 User important declarations。
- 使用选择器特异性 specificity(a-b-c-d 数字系统)。
布局 Layout
渲染器在创建完成并添加到渲染树时,并不包含位置和大小信息。计算这些值的过程称为布局(Webkit 使用术语)或重排(Reflow,Gecko 使用术语)。
布局是一个递归的过程。它从根渲染器(对应 <html> 元素)开始,然后递归遍历部分或所有的框架层次结构,为每一个需要计算的渲染器计算几何信息。
为避免对所有细小更改都进行整体布局,浏览器采用了一种“dirty bit”系统。当某个渲染器发生了更改,自身及其子代被标注为“dirty”,则表示需要布局。
只对 dirty 渲染器布局叫增量布局(incremental layout),异步执行。Gecko 将增量布局的“reflow 命令”加入队列,而调度程序会触发这些命令的批量执行。Webkit 也有用于执行增量布局的计时器。
全局布局通常是同步触发的。
绘制 Painting
遍历渲染树,并调用渲染器的“paint”方法,将渲染器的内容显示在屏幕上。绘制工作是使用 UI 基础组件完成的。
和布局一样,绘制也分为全局和增量两种。
绘制的顺序是元素进入堆栈样式上下文的顺序。块呈现器的堆栈顺序如下 background-color > background-image > border > children 子代 > outline。
文章参考