浏览器工作原理及内核详解

浏览器主要功能

浏览器第一个主要功能就是从服务器下载Web资源并在浏览器窗口中将它呈现。这些资源可以是 HTML 文档,也可以是 PDF,图像等。而浏览器解析并显示 HTML 文档与如何处理 CSS 由 W3C组织 规范制定。以下是浏览器的组成要件:

  1. UI(用户界面):除了网页内容窗体以外的区域,包括地址栏、状态栏、工具栏、后退/前进按钮。

2.浏览器引擎:用户界面和呈现引擎之间传递指令。

  1. 呈现(渲染)引擎:负责解析并显示请求内容。

  2. 网络组件:负责网络请求,如 HTTP 请求。

  3. UI 后端:负责绘制基本的小部件,如系统模态弹窗。

  4. JavaScript 解析器:负责解析和执行 JavaScript 代码。

  5. 数据存储:浏览器需要保存在硬盘上的各种数据,如 Cookie、IndexedDB。

渲染引擎的基本流程

  1. 解析 HTML 文档构造 DOM 树。
  2. “渲染”过程将解析外部 CSS 文件和元素的样式属性,渲染树包含多个视觉效果并以正确的显示顺序的矩形。
  3. “布局”过程将每个节点定位在屏幕的确切坐标上。
  4. “绘画”过程遍历每个节点使用 UI后端涂漆(绘制)。

这是一个渐进的过程,为了更好的用户体验,渲染引擎会一边解析 HTML 一边开始构建和布局渲染,这个阶段一直保持与服务器通讯。

解析和 DOM 树构建

能把文档转换为有意义的结构并让计算机能够理解及使用,我们称之为解析,它必须遵循特定的语法规则并与上下文无关,解析的结果是具有文档结构的节点树,也叫作解析树或语法树。

解析器

解析可以分为两个子过程:词法分析语法分析

  • 词法分析:
    将输入分解成标记的过程,标记就是语言词汇(有效构建块的集合)。在人类语言中,相当于字典中的单词。

  • 语法分析:
    应用语言语法规则的过程。

解析器就是负责将输入转换为有效标记,并根据语言的语法规则分析该文件结构并构造出解析树,词法分析会知道如何去除不相关的字符,比如空格和换行符。

有时候解析树并不是最终产品,通常还可以用于翻译,将输入文档转换为另一种格式。编译器会首先将源代码解析成解析树,然后将该树翻译成机器代码。

HTML 解析器

HTML 解析器的工作就是将 HTML 标记解析为解析树,HTML 的词汇及语法由 W3C组织 规范制定。HTML 是具有与上下文相关的语法组成,<u>常规解析器无法解析 HTML文档</u>。在与 XML 相比,两者也存在一些差异,HTML 语法“随意”、“松散”,XML 则“严谨”、“苛刻”,所以,<u> XML 解析器无法解析 HTML</u>。

HTML DTD

DTD 文档类型定义用来定义 SGML 家族的语法,而 HTML 是基于 SGML 的。DTD 中包含允许出现的元素、属性和层次结构的定义,<u>这也说明了 HTML 是与上下文有关的语言标记</u>。

DOM

DOM 文档对象模型提供了一套 HTML 文档元素与外部世界连接的标准接口,这些规范也由 W3C组织 制定,树的根节点就是“文档”对象。

HTML 解析算法

HTML 解析算法分为两个阶段:标记化和树构建。

标记化算法

标记化就是词法分析。该算法状态机来表示,每个状态接收输入流的一个或多个字符,并根据当前字符更新下一个状态。接收相同字符,对于正确的下一状态可能产生不同的结果。我们来看一个简单的例子:

<html>
  <body>
    Hello world!
  </body>
</html>

初始状态是“数据状态”。当遇到“<”字符时,状态更改为“标记打开状态”。接收“h”字符会创建一个“起始标记”,状态更改为“标记名称状态”,每消费一个字符都会附加在这个令牌名称上,保持这个状态直到遇见“>”字符,创建了一个“html”标记并发送。

接着状态回到“数据状态”,“<body>”标记将按相同的步骤处理。到目前为止,一共有两个标记被发送。

继续回到“数据状态”,接收“Hello world”的“H”字符会创建一个“字符标记”,一共有11个“字符标记”被发送。

接收“</body>”的“<”字符,状态再次回到“标记打开状态”,当接收下一个输入“/”会创建“结束标记”,状态更改为“标记名称状态”,继续接收“body”中的每个字符,直到遇见“>”,然后将新的标记被发送,状态回到“数据状态”,“</html>”输入执行相同处理。

树构建算法

在这个阶段,对于每个标记与规范中定义的 DOM 元素相关,则将该标记追加到根节点为 Document 对象的 DOM 树上。该算法以状态机来表示。

<html>
  <body>
    Hello world!
  </body>
</html>

对应标记化阶段的第一模式是“初始模式”,接收发送过来的 html 起始标记,状态更改为“** Html 之前**”模式,将创建一个 HTMLHtmlElement 元素,并追加到 Document 对象上。

状态更改为“** Head 之前**”,显然没有收到 head 起始标记而是 body 起始标记,将隐式创建一个 HTMLHeadElement元素,并追加到 Document 对象上。

body 起始标记被重新处理,状态从为“** Head 之后”更改为“ Body 之前”模式,创建一个 HTMLBodyElement元素,并追加到 Document 对象上,同时状态更改为“在 Body 里**”。

现在接收到“Hello world”一系列字符标记,第一个“H”将创建和插入到“文本”节点,其它字符将附加到该节点上。

body 结束标记接收到后,状态更改为“** Body 之后”模式,接着又将接收到 html 结束标记,促使状态更改为“ Html 之后**”模式。接收文件的结束标记后将结束解析。

浏览器此时会将文档状态标记为“交互式”,并开始解析处于“延迟”模式的脚本程序,这些脚本将在解析文档后被执行,然后文档状态设置为“完成”,并触发“加载”事件。

浏览器容错

你将不会在 HTML 页面上看到“语法无效”的错误,浏览器会自动修复无效内容并继续,这种错误处理机制在其它浏览器中的做法是非常一致的,它不属于标准的一部分,就像书签和后退/前进按钮,都是浏览器多年来总结的经验。

CSS解析器

与 HTML 不同,CSS 是一个上下文无关的语法。语法在 BNF 中描述如下:

规则集
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;
选择器
  : simple_selector [ combinator selector | S+ [ combinator selector ] ]
  ;
简单选择器
  : element_name [ HASH | class | attrib | pseudo ]*
  | [ HASH | class | attrib | pseudo ]+
  ;
类
  : '.' IDENT
  ;
元素名
  : IDENT | '*'
  ;
属性
  : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
    [ IDENT | STRING ] S* ] ']'
  ;
伪
  : ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
  ;

每个 CSS 样式表会被解析为一个 StyleSheet 对象,每个对象里包含 CSS 规则。CSS 规则对象包含选择器和声明对象以及 CSS 语法相对应的其它对象。

解析脚本和样式表的顺序

脚本

<u>网络的模型是同步的,当解析器执行<script>标记时,会立刻解析并执行脚本。如果是外部脚本,文档会停止解析,直到资源被获取。</u>你也可以将脚本标记为“defer”,那么它就不会停止文档解析,并在解析后执行脚本程序。HTML5 添加了一个选项来标记脚本为异步,因此可以视为不同线程在处理,这种策略叫预解析。

样式表

从概念上看,样式表不会改变 DOM 树,因此没有理由等待它们并停止文档解析。但是,有一个情况,当脚本在文档解析阶段请求样式信息,如果样式没有加载并解析,那么就会照成很多问题。

当仍在加载和解析样式表时,Firefox 会阻止所有脚本,Webkit 则只在脚本试图访问某些可能受尚未加载样式表影响时才阻止脚本。

渲染树建设

当 DOM 树被构建后,浏览器会构造另外一棵树,即渲染树。<u>该树的目的是以正确的顺序绘制内容并呈现视觉元素给用户。</u>

渲染器

Firfox 渲染树中的元素称为“框架”,Webkit 则使用术语渲染器或渲染对象,目的是如何布局和绘制渲染树。

每个渲染器通常对应节点的 CSS 框的矩形区域,它包含几何信息有宽度、高度和位置。

框的类型会受到与节点相关的“display”样式属性影响。

Webkit RenderObject 基类定义如下:

class RenderObject{
    virtual void layout();
    virtual void paint(PaintInfo);
    virtual void rect repaintRect();
    Node* node;  // DOM节点
    RenderStyle* style;  //计算的样式
    RenderLayer* containgLayer; //包含z-index层
}

以下为Webkit 如何根据属性决定应该为 DOM 节点创建什么类型的渲染器。

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个渲染器,一个用于显示区域,一个用于下拉列表, 一个用于按钮。另外多行文本也需要添加额外的渲染器。

3.不合理地嵌套标记会产生额外渲染器
W3C 组织规范规定内联元素必须包含块元素或内联元素,在混合文本嵌套混乱的情况下,会创建匿名块渲染器包含内联元素。

4.渲染器对应 DOM 节点,但不在树上的相同位置。
浮动和绝对定位的元素处于正常文档流之外,放置在树中的其它地方并映射到正确的框架,而放在原位上的是占位符。

样式计算

构建渲染树需要计算每个渲染对象的表现属性,这是通过计算每个元素的样式属性来完成的。

样式属性包含各种来源样式表,内联样式元素和 HTML 中表现属性(如“bgcolor”属性)。后者被翻译为匹配的CSS样式属性。

样式表的来源包含浏览器的默认样式表,网页作者和用户自定义样式表,例如,通过在“Firefox Profile”文件夹中放置样式表来完成的。

规则简化匹配

样式表解析完后,系统会根据选择器将 CSS 规则添加到某个哈希表中,包括 ID 表、Class 表、Tag 表。匹配原则就是先找出那些根据键提取的规则,然后再进行正确的匹配。

层叠匹配规则

层叠顺序优先级:

  1. 浏览器声明
  2. 用户普通声明
  3. 作者普通声明
  4. 作者重要声明
  5. 用户重要声明

特征性:

  • “Style”属性,记为 1,否则记为 0(=a)
  • 选择器 ID 属性的个数(=b)
  • 选择器中其他属性和伪类的个数(=c)
  • 选择器中元素名称和伪元素的个数(=d)

布局

当创建渲染器并将其添加到树中时,它并没有位置和大小,计算这些值的阶段称为布局回流

  1. HTML 采用基于流的布局模型
    这意味着大多数时候只要一次遍历就能计算出几何信息,之后出现的元素通常不会影响较早的元素几何,所以布局就是从左到右,从上到下顺序遍历文档的。

  2. 坐标相对于根框架
    根渲染器的位置是从顶部左侧坐标 0,0 开始,它的尺寸是视口(浏览器窗口的可视部分)。

  3. 渲染器的方法
    所有渲染器都有一个“布局”或“回流”方法,每个渲染器都会调用其需要进行布局的子代“布局”方法。

脏位系统

为了不对每个小的改变做一个完整的布局,浏览器使用“脏位”系统。目的就是将被改变或添加的渲染器将其自身和后代标记为“脏”,也就是需要布局。

全局和增量布局

1.全局布局
触发“全局布局”的原因主要是影响所有渲染器的全局样式被更改,如字体大小,或是屏幕调整大小。

2.增量布局
当渲染器“脏”时,触发(异步)增量布局。例如,额外内容来自网络并添加到 DOM 树后,新的渲染器被附加到渲染树。

布局过程

布局通常具有以下模式:

  1. 父渲染器确定其自身的宽度。
  2. 父渲染器一次处理子渲染器,并且:
  3. 放置子渲染器(设置其 x 和 y 坐标)。
  4. 如果需要调用子布局(它们是脏的,或者全局布局或一些其他原因) ,这将计算子渲染器的高度。
  5. 父级使用子级累加高度和边距和填充的高度来设置自己的高度 - 这将可供父渲染器的父级使用。
  6. 将其脏位设置为false。

Firefox使用“状态”对象(nsHTMLReflowState)作为布局参数(称为“回流”)。其中状态包括父级宽度。
Firefox布局的输出是一个“metrics”对象(nsHTMLReflowMetrics)。它将包含计算得出的渲染器高度。

宽度计算

渲染器的宽度是使用容器块的宽度,渲染器的样式“width”属性,边距和边框计算的。
例如以下div的宽度:

<div style =“width:30%”/>

将由Webkit计算如下(类RenderBox方法calcWidth):

  • 容器宽度是容器availableWidth和0的最大值。这种情况下的availableWidth是contentWidth,计算公式为:
clientWidth() -  paddingLeft() -  paddingRight()

clientWidth和clientHeight表示对象的内部,不包括边框和滚动条。

  • 元素width是“width”样式属性。它将通过计算容器宽度的百分比计算为绝对值。
  • 然后添加水平边框和padding。

这是“首选宽度”的计算。然后将计算最小和最大宽度。
如果首选宽度高于最大宽度,将使用最大宽度。如果它低于最小宽度(最小不可破碎单位),则使用最小宽度。值会被缓存,以防需要布局但宽度不变的情况。

换行

当渲染器在布局过程中需要换行,会立即停止布局,并告知其父代需要换行。父代将会创建额外的渲染器并调用布局。

绘制

在绘制阶段使用 UI 基础结构组件遍历渲染树,并且调用渲染器“绘制”方法以在屏幕上显示它们的内容。

全局和增量

像布局一样,绘制也可以是全局(整个树被绘制)或增量。

在增量绘制中,一些渲染器改变但不影响整个渲染树。更改后的渲染器会使其在屏幕上的矩形无效。这使得OS将其看作“脏区域”并且生成“绘制”事件。OS 巧妙地将几个区域合并为一个。

在Chrome 中它更复杂,因为渲染器不在主进程中。Chrome 在一定程度上模拟了 OS 的行为。

展示层会监听这些事件,并将消息委托给渲染根节点。遍历树直到找到相关渲染器。它会重新绘制自己(通常包括其子代)。

绘制顺序

CSS2 定义了绘制的顺序。其实就是元素进入堆栈样式上下文的顺序。这个顺序影响绘制,因为堆栈是从后到前绘制的。
块渲染器的堆叠顺序是:

  1. background color
  2. background image
  3. border
  4. children
  5. outline

Firefox显示列表

Firefox浏览渲染树并为已绘制的矩形构建一个显示列表。它包含与矩形相关的渲染器以及正确的绘制顺序(渲染器的背景,边框等)。
这样,等到树需要重绘时,只需遍历一次渲染树(绘制所有背景,图片,边框等)。

Webkit矩形存储

重新绘制之前,Webkit 将旧矩形另存为一张位图。然后它只绘制新旧矩形之间的增量。

动态更改

浏览器会尝试做出响应更改的最小可能性。

  1. 对元素的颜色的更改仅影响元素的重绘。
  2. 元素位置更改,影响其子元素(可能还有兄弟元素)进行布局和重绘。
  3. 新增的 DOM 节点导致节点的布局和重绘。

一些重大变化,如增加“html”元素的字体大小,导致整个树的缓存无效,使得整个渲染树进行重新布局和绘制。

渲染引擎的线程

呈现引擎是单线程的。除了网络以外,几乎所有的操作都在同一单线程中进行。

网络操作可由多个并行线程同时执行(一般是 2 至 6个)。

事件循环

浏览器的主线程是一个事件的无限循环,永远处于接受处理状态,并且等待事件(如布局和绘制)。以下是主要事件循环的 Firefox 代码:

while(!mExiting) NS_ProcessNextEvent(thread);

CSS2 可视化模型

画布

“画布”是用来呈现格式化结构的空间,也就是供浏览器绘制内容的区域。

CSS框模型

针对文档树中的元素生成,根据可视化格式模型进行布局的矩形框。每个框都有一个内容区域(如文本、图片等),还有可选的周围填充、边框和边距。所有元素都有一个“display”属性,决定它们所对应生成的框类型。

定位方案

定位方案由“position”属性和“float”属性设置的。

  • 普通:根据对象在文档的位置进行定位,该对象在渲染树和在 DOM 树上的位置相似,并根据其框类型和尺寸进行布局,值是“static”或“relative”。
  • 浮动:对象先按普通进行布局,然后尽可能向左或向右布局。
  • 绝对:对象在渲染树的位置和在 DOM 树上的位置不同,值是“absolute”或“fixed”。

指定位置使用:top、bottom、left、right。

框的布局方式是由以下因素决定的:

  • 框类型
  • 框尺寸
  • 定位方案
  • 外部信息,例如图片大小和屏幕大小

框类型

block 框

  • 形成一个 block(矩形区域),在浏览器窗口中拥有其自己的矩形区域。
  • 采用垂直格式排列。

inline 框

  • 没有自己的 block,但是位于容器 block 内。
  • 采用水平格式排列。

定位

  1. 相对定位:先按照普通文档流定位,然后再跟进偏移量进行移动。
  2. 浮动:浮动框移动到行的左边或右边。其它框会浮动在它的周围。
  3. 绝对和固定定位:元素不参与普通文档流,尺寸是相对于容器而言的,固定定位中容器是可视区域。

分层展示

由 CSS 属性 z-index 属性指定。z-index 属性的优先级越高,那么移动到根框所保持的堆栈中更靠前的位置。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容