Web 浏览器概述

历史

JavaScript 最初是为了 Web 浏览器创立的。

下图是运行在 Web 浏览器宿主环境中的一个鸟瞰



作为根对象的 window 有两个作用:

  1. 作为 JavaScript 代码的全局(global)对象。
  2. 代表浏览器的对象,提供方法来操作浏览器。

主要组件

浏览器的主要功能是展示用户选择的 web 资源,从服务器请求并在浏览器窗口显示。资源的位置由用户使用 URI(统一资源标示符)指定。

组件包括:

渲染引擎采用了单线程,几乎所有操作(除了网络操作)都是在单线程中进行的。在 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)

国产浏览器一般基于以上主流浏览器的单内核或双内核开发。

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 如此流行的原因:包容错误,方便 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 元素相对应的,但并非一一对应。

在 Gecko 中,会针对 DOM 更新注册展示层,作为侦听器。展示层将框架创建工作委托给 FrameConstructor。在 Webkit 中,解析样式和创建渲染器的过程称为“附加”(attachment)。每个 DOM 节点都有一个“attach”方法。附加是同步进行的,将节点插入 DOM 树需要调用新的节点“attach”方法。

处理 html 和 body 标记就会构建渲染树根节点。这个根节点渲染对象对应于 CSS 规范中所说的最上层容器 block,包含了其他所有 block。它的尺寸就是视口(viewport),即浏览器窗口显示区域的尺寸。Gecko 称之为 ViewPortFrame,Webkit 称之为 RenderView。

样式计算存在以下难点:

浏览器解决办法:

布局 Layout

渲染器在创建完成并添加到渲染树时,并不包含位置和大小信息。计算这些值的过程称为布局(Webkit 使用术语)或重排(Reflow,Gecko 使用术语)。

布局是一个递归的过程。它从根渲染器(对应 <html> 元素)开始,然后递归遍历部分或所有的框架层次结构,为每一个需要计算的渲染器计算几何信息。

为避免对所有细小更改都进行整体布局,浏览器采用了一种“dirty bit”系统。当某个渲染器发生了更改,自身及其子代被标注为“dirty”,则表示需要布局。

只对 dirty 渲染器布局叫增量布局(incremental layout),异步执行。Gecko 将增量布局的“reflow 命令”加入队列,而调度程序会触发这些命令的批量执行。Webkit 也有用于执行增量布局的计时器。

全局布局通常是同步触发的。

绘制 Painting

遍历渲染树,并调用渲染器的“paint”方法,将渲染器的内容显示在屏幕上。绘制工作是使用 UI 基础组件完成的。

和布局一样,绘制也分为全局和增量两种。

绘制的顺序是元素进入堆栈样式上下文的顺序。块呈现器的堆栈顺序如下 background-color > background-image > border > children 子代 > outline。




文章参考