浏览器工作原理

最终决定浏览器表现出来的页面效果的差异是:渲染引擎 Rendering Engine(也叫做排版引擎),也就是我们通常所说的“浏览器内核”,负责解析网页语法(如HTML、JavaScript)并渲染、展示网页。相同的代码在不同的浏览器呈现出来的效果不一样,那么就很有可能是不同的浏览器内核导致的。

我们来看一下加载页面时浏览器的具体工作流程(图一):

(图一)

1、解析HTML以重建DOM树(Parsing HTML to construct the DOM tree ):渲染引擎开始解析HTML文档,转换树中的标签到DOM节点,它被称为“内容树”。

2、构建渲染树(Render tree construction):解析CSS(包括外部CSS文件和样式元素),根据CSS选择器计算出节点的样式,创建另一个树 —- 渲染树。

3、布局渲染树(Layout of the render tree): 从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标。

4、绘制渲染树(Painting the render tree) : 遍历渲染树,每个节点将使用UI后端层来绘制。

主要的流程就是:构建一个dom树,页面要显示的各元素都会创建到这个dom树当中,每当一个新元素加入到这个dom树当中,浏览器便会通过css引擎查遍css样式表,找到符合该元素的样式规则应用到这个元素上。

对此,在CSS书写过程中,总结出如下性能提升的方案:

  1. 避免使用通配规则      如    *{} 计算次数惊人!只对需要用到的元素进行选择
  2. 尽量少的去对标签进行选择,而是用class     如:#nav li{},可以为li加上nav_item的类名,如下选择.nav_item{}
  3. 不要去用标签限定ID或者类选择符   如:ul#nav,应该简化为#nav
  4. 尽量少的去使用后代选择器,降低选择器的权重值  后代选择器的开销是最高的,尽量将选择器的深度降到最低,最高不要超过三层,更多的使用类来关联每一个标签元素
  5. 考虑继承 了解哪些属性是可以通过继承而来的,然后避免对这些属性重复指定规则

一、浏览器工作原理(简化版)

1、浏览器用来干什么用

浏览器的主要功能是将用户请求访问的web资源呈现出来,它需要从服务器请求资源,并将其显示在浏览器窗口中,资源的格式通常是HTML,也包括PDF、image及其他格式。用户用URI(Uniform Resource Identifier 统一资源标识符)来指定所请求资源的位置。
HTML和CSS规范中规定了浏览器解释html文档的方式,由W3C组织对这些规范进行维护,W3C是负责制定web标准的组织。

2、浏览器的主要构成组件

直接上图:

  1. 用户界面- 包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口之外的其他部分
  2. 浏览器引擎- 用来查询及操作渲染引擎的接口
  3. 渲染引擎- 用来显示请求的内容,例如,如果请求内容为html,它负责解析html及css,并将解析后的结果显示出来
  4. 网络- 用来完成网络调用,例如http请求,它具有平台无关的接口,可以在不同平台上工作
  5. UI后端- 用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口
  6. JS解释器- 用来解释执行JS代码
  7. 数据存储- 属于持久层,浏览器需要在硬盘中保存类似cookie的各种数据,HTML5定义了web database技术,同样是一种轻量级完整的客户端存储技术

3、数据渲染(The rendering engine)

网页浏览器的页面渲染引擎也被称为排版引擎,它负责取得网页的内容(HTML、XML、图象等等)、整理信息(例如加入CSS等),以及计算网页的显示方式然后会输出至显示器或打印机。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要排版引擎。

Firefox使用Gecko——Mozilla自主研发的渲染引擎,Safari和Chrome都使用webkit。Webkit是一款开源渲染引擎,它本来是为linux平台研发的,后来由Apple移植到Mac及Windows上,相关内容请参考http://webkit.org。

4、数据渲染流程(The main flow)

渲染引擎开始解析html,并将标签转化为内容树中的dom节点。接着,它解析外部CSS文件及style标签中的样式信息。这些样式信息以及html中的可见性指令将被用来构建另一棵树——render树。
Render树由一些包含有颜色和大小等属性的矩形组成,它们将被按照正确的顺序显示到屏幕上。
Render树构建好了之后,将会执行布局过程,它将确定每个节点在屏幕上的确切坐标。再下一步就是绘制,即遍历render树,并使用UI后端层绘制每个节点。

浏览器工作原理_第1张图片值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

5、DOM

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

<html>
<body>
<p<Hello DOM</p>
<div><img src=”example.png” /></div>
</body>
</html>

解析完后的DOM树为:

6、解析算法(The parsing algorithm)

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

Html5规范中描述了这个解析算法,算法包括两个阶段——符号化及构建树。
符号化是词法分析的过程,将输入解析为符号,html的符号包括开始标签、结束标签、属性名及属性值。
符号识别器识别出符号后,将其传递给树构建器,并读取下一个字符,以识别下一个符号,这样直到处理完所有输入。

7、树的构建算法(Tree construction algorithm)

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

来看一下示例中树的创建过程:

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

构建树这一阶段的输入是符号识别阶段生成的符号序列。
首先是“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”模式,当接收到文件结束符时,整个解析过程结束。

8、渲染树的构造(Render tree construction)

当Dom树构建完成时,浏览器开始构建另一棵树——渲染树。渲染树由元素显示序列中的可见元素组成,它是文档的可视化表示,构建这棵树是为了以正确的顺序绘制文档内容。
Firefox将渲染树中的元素称为frames,webkit则用renderer或渲染对象来描述这些元素。

一个渲染对象知道如何布局以及绘制自己和它的children。 RenderObject是Webkit的渲染对象基类,它的定义如下:

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属性决定某个节点创建何种类型的渲染对象。

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”方法,使渲染对象指向不包含几何信息的样式对象。

9、样式计算(Style Computation)

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

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

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

样式数据是非常大的结构,保存大量的样式属性会带来内存问题。
如果不进行优化,找到每个元素匹配的规则会导致性能问题,为每个元素查找匹配的规则都需要遍历整个规则表,这个过程有很大的工作量。选择符可能有复杂的结构,匹配过程如果沿着一条开始看似正确,后来却被证明是无用的路径,则必须去尝试另一条路径。
例如,下面这个复杂选择符
div div div div{…}

这意味着规则应用到三个div的后代div元素,选择树上一条特定的路径去检查,这可能需要遍历节点树,最后却发现它只是两个div的后代,并不使用该规则,然后则需要沿着另一条路径去尝试

应用规则涉及非常复杂的级联,它们定义了规则的层次。

10、布局(Layout)

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

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

坐标系统相对于根frame,使用top和left坐标。

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

二、相关渲染引擎内核介绍

1、Gecko:Mozilla Firefox

标准支持:

  • HTML 4.01 (支持部分HTML 5)
  • XML 1.0
  • XHTML 1.1
  • MathML
  • CSS Level 2.1(支持部份CSS 3)
  • DOM Level 1和2(支持部份DOM 3)
  • RDF
  • JavaScript 1.8(ECMAScript 3,支持部分ECMAScript 5)由SpiderMonkey实现
  • E4X
  • SVG(支持部份SVG 1.1)
  • XSLT和XPath由TransforMiiX实现
  • XForms(借由官方的扩展)

 

2、WebKit:Apple Safari及Google Chrome

WebCore
WebCore是一个由WebKit专案所开发的布局(Layout)、渲染(Rendering)及HTML和SVG的DOM函式库,完整的程式码皆由GNU宽通用公共许可证所授权,WebKit框架包装了WebCore及JavaScriptCore,并提供一个Objective-C应用程序接口来接介由C++所开发的WebCore渲染引擎及JavaScriptCore脚本引擎,透过Cocoa API就可以在应用程式中很简单的使用这些元件。之后的版本同时包含了一个跨平台的C++抽象平台,并且提供各种API使用。
WebKit通过Acid2及Acid3的测试,包含完美像素的渲染(pixel-perfect rendering)以及没有任何时间及不顺的问题。

JavaScriptCore
JavaScriptCore是一个在WebKit中提供JavaScript引擎的框架,而且在OS X作为其他内容的脚本引擎[10][66]。JavaScriptCore最初是为KDE的JavaScript引擎(KJS)函式库及PCRE正则表达式函式库,JavaScriptCore从KJS及PCRE复刻之后,已比原先进步了许多,有了新的特色以及极大的效能改进。

Drosera
Drosera是一个JavaScript调试工具,它被包含在每日编译的WebKit版本内。

3、Trident:Internet Explorer

在Internet Explorer第七版中,微软对Trident排版引擎做了的重大的变动,除了加入新的技术之外,并增加对网页标准的支持。尽管这些变动已经在相当大的程度上落后了其它的排版引擎,如Gecko、WebCore、KHTML、Webkit及Presto。

以Trident为核心的浏览器
Avant Browser(前身为IeOpera)
Maxthon(前身为MyIE3.2、MyIE2)
GreenBrowser(前身亦为MyIE3.2)
TouchNet Browser
腾讯TT
GOSURF
世界之窗(TheWorld Browser)
MiniIE
Sleipnir
MyIE(新版4.x为GreenBrowser作者发布,3.2及之前版本为Maxthon、GreenBrowser、iTreeSurf等浏览器的前身)
iTreeSurf(LovelyTree,前身亦为MyIE3.2)
(注:中国大陆的大部分浏览器都使用Trident 排版引擎)

解析

解析文档是指将文档转化成有意义的结构,也就是可让代码理解和使用的结构。

解析得到的结果通常是代表了文档结构的节点树,它称作解析树或者语法树。

HTML解析

HTML语法定义

常规解析器都不适用于HTML,HTML并不能很容易地用解析器所需的的上下文无关的语法来定义。

有一种可以定义HTML的正规格式:DTD(Document Type Definition,文档类型定义),但它还是与上下文无关的语法。原因是HTML的语法处理很宽容,允许省略某些隐匿添加的标记,有时还能省略一些起始或者结束标记等等。

DOM

解析器输出的“解析树”是由DOM元素与属性节点构成的树结构。DOM是文档对象模型(Document Object Model)的缩写。它是HTML文档的对象表示,同时也是外部内容与HTML元素之间的接口。

HTML5规范详细地打描述了解析算法。此算法由两个阶段组成:标记化和树构建。

解析算法

我们在之前章节已经说过,HTML 无法用常规的自上而下或自下而上的解析器进行解析。

原因在于:

  • 语言的宽容本质。
  • 浏览器历来对一些常见的无效 HTML 用法采取包容态度。
  • 解析过程需要不断地反复。源内容在解析过程中通常不会改变,但是在 HTML 中,脚本标记如果包含 document.write,就会添加额外的标记,这样解析过程实际上就更改了输入内容。

HTML的解析算法由两个阶段组成:标记化和树构建

标记化算法
<html>
    <body>
        Hello world
    </body>
</html>

初始状态是数据状态,当遇到字符<时,状态更改为“标记打开状态”。接收一个a-z字符会创建“起始标记”,状态更改为“标记名称状态”。这个状态会一直保持到接收>。在此期间接收的每个字符都会附加到新的标记名称上。在本例中,我们创建的标记是html标记。

遇到>标记时,会发送当前的标记,状态发回“数据状态”。<body>标记也会进行同样的处理。目前htmlbody标记均已发出。现在我们回到“数据状态”。接收到Hello world中的H字符时,将创建并发送字符标记,直到接收</body>中的<。我们将为Hello world中的每个字符都发送一个字符标记。

现在我们回到“标记打开状态”。接收下一个输入字符/时,会创建end tag token并改为“标记名称状态”。我们会再次保持这个状态,直到接收>。然后将发送新的标记,并回到“数据状态”。</html>输入也会进行同样的处理。

树构建算法

树构建阶段的输入是一个来自标记化阶段的标记序列。第一个模式是“initial mode”。接收HTML标记后转为“before html”模式,并在这个模式下重新处理此标记。这样会创建一个HTMLHtmlElement元素,并奖其附加到Document根对象上。

后续状态:

  1. “before head”,接收“body”标记,创建HTMLHeadElement,添加到树中。
  2. “in head”模式,然后转入“after head”模式。创建并插入HTMLBodyElement,然后模式转变为“body”
  3. 接收body中的字符串,然后创建并插入“text”节点,其他字符也将附加到该节点
  4. 接收body结束标记,触发after body模式,接收剩余的HTML结束标记。解析过程结束。

解析结束后的操作

文档标记为交互状态,可以解析处于“deferred”模式的脚本。

浏览器容错机制

浏览器会纠正任何无效内容,然后继续工作。Webkit在HTML解析器类的形状注释中对此做了相应的概括:

解析器对标记化输入内容进行解析,以构建文档树。如果文档的格式正确,就直接进行解析。遗憾的是,我们不得不处理很多格式错误的 HTML 文档,所以解析器必须具备一定的容错性。

我们至少要能够处理以下错误情况:

  1. 明显不能在某些外部标记中添加的元素。在此情况下,我们应该关闭所有标记,直到出现禁止添加的元素,然后再加入该元素。
  2. 我们不能直接添加的元素。这很可能是网页作者忘记添加了其中的一些标记(或者其中的标记是可选的)。这些标签可能包括:HTML HEAD BODY TBODY TR TD LI(还有遗漏的吗?)。
  3. 向 inline 元素内添加 block 元素。关闭所有 inline 元素,直到出现下一个较高级的 block 元素。
  4. 如果这样仍然无效,可关闭所有元素,直到可以添加元素为止,或者忽略该标记。

CSS解析

词法语法(词汇)是针对各个标记用正则表达式定义的:

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}*

语法是采用BNF格式描述的。什么是BNF格式?豆瓣里面有解释。

Webkit CSS 解析器

呈现树构建(Render Tree)

在 DOM 树构建的同时,浏览器还会构建另一个树结构:呈现树。这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的作用是让您按照正确的顺序绘制内容。

在Webkit中,如果一个元素需要创建特殊的呈现器,就会替换createRenderer方法。呈现器所指向的样式对象中包含了一些和几何无关的信息。

呈现树与DOM树的关系

浏览器工作原理_第2张图片

样式计算

共享样式数据

Webkit 节点会引用样式对象 (RenderStyle)。这些对象在某些情况下可以由不同节点共享。这些节点是同级关系,并且:

  • 这些元素必须处于相同的鼠标状态(例如,不允许其中一个是“:hover”状态,而另一个不是)
  • 任何元素都没有 ID
  • 标记名称应匹配
  • 类属性应匹配
  • 映射属性的集合必须是完全相同的
  • 链接状态必须匹配
  • 焦点状态必须匹配
  • 任何元素都不应受属性选择器的影响,这里所说的“影响”是指在选择器中的任何位置有任何使用了属性选择器的选择器匹配
  • 元素中不能有任何 inline 样式属性
  • 不能使用任何同级选择器。WebCore 在遇到任何同级选择器时,只会引发一个全局开关,并停用整个文档的样式共享(如果存在)。这包括 + 选择器以及 :first-child 和 :last-child 等选择器。

你可能感兴趣的:(浏览器)