浏览器是怎样工作的:渲染引擎,HTML解析(连载二)

标签: 前端开发 HTML 浏览器 渲染引擎 | 发表时间:2012-05-07 05:22 | 作者:hfliu
出处:http://ued.ctrip.com/blog

渲染引擎

渲染引擎的职责是……渲染,也就是把请求的内容显示到浏览器屏幕上。

默认情况下渲染引擎可以显示HTML,XML文档以及图片。 通过插件(浏览器扩展)它可以显示其它类型文档。比如使用PDF viewer插件显示PDF文件。我们会在一个专门的章节讨论插件与扩展。在这一节我们将专注渲染引擎的主要用途——显示用CSS格式化的HTML与图片。

各种渲染引擎

我们提到的Firefox, Safari两种浏览器构建于两种渲染引擎之上:Firefox使用Gecko —— Mozilla自家的渲染引擎;Safari 和 Chrome 都使用 Webkit。

Webkit 是一个开源的渲染引擎,它源自Linux平台上的一个引擎,经过Apple公司的修改可以支持Mac与Windows平台。更多信息可以参考:  http://webkit.org/ 。

主要流程

渲染引擎开始于从网络层获取请求内容,一般是不超过8K的数据块。接下来就是渲染引擎的基本工作流程:

图 2:渲染引擎的基本工作流程(解析HTML构建DOM树,渲染树构建,渲染树布局,绘制渲染树)。

渲染引擎会解析HTML文档并把标签转换成内容树中的DOM节点。它会解析style元素和外部文件中的样式数据。样式数据和HTML中的显示控制将共同用来创建另一棵树—— 渲染树

渲染树包含带有颜色,尺寸等显示属性的矩形。这些矩形的顺序与显示顺序一致。

渲染树构建完成后就是” 布局“处理,也就是确定每个节点在屏幕上的确切显示位置。 下一个步骤是  绘制 —— 遍历渲染树并用UI后端层将每一个节点绘制出来。

一定要理解这是一个缓慢的过程,为了更好的用户体验,渲染引擎会尝试尽快的把内容显示出来。它不会等到所有HTML都被解析完才创建并布局渲染树。它会 在处理后续内容的同时把处理过的局部内容先展示出来。

主要流程示例

 


图 3:Webkit主要流程
图 4:Mozilla的Gecko渲染引擎主要流程( 3.6)

从图3和图4中可以看出,尽管Webkit与Gecko使用略微不同的术语,这个过程还是基本相同的。
Gecko 里把格式化好的可视元素称做“帧树”(Frame tree)。每个元素就是一个帧(frame)。 Webkit 则使用”渲染树”这个术语,渲染树由”渲染对象”组成。Webkit 里使用”layout”表示元素的布局,Gecko则称为”Reflow”。Webkit使用”Attachment”来连接DOM节点与可视化信息以构建渲染树。一个非语义上的小差别是Gecko在HTML与DOM树之间有一个附加的层 ,称作”content sink”,是创建DOM对象的工厂。我们会讨论流程中的每一部分。

 

解析

因为解析是渲染引擎中一个很重要的处理,我们会讲的略深入一些。让我们从一个小的解析介绍开始。

解析一个文档意味着把它翻译成有意义的结构以供代码使用。解析的结果通常是一个表征文档的由节点组成的树,称为解析树或句法树。

示例——解析表达式”2 + 3 – 1″可以返回下面的树:


图 5:数学表达式树节点

语法

解析是基于文档所遵循的语法规则——书写所用的语言或格式——来进行的。每一种可以解析的格式必须由确定的语法与词汇组成。这被称之为 上下文无关语法。 人类语言并非此种语言,所以不能用常规的解析技术来解析。

解析器——词法分析器组合

解析器有两个处理过程——词法分析与句法分析。

词法分析负责把输入切分成符号序列,符号是语言的词汇——由该语言所有合法的单词组成。

句法分析是对该语言句法法则的应用。

解析器通常把工作分给两个组件——分词程序负责把输入切分成合法符号序列,解析程序负责按照句法规则分析文档结构和构建句法树。词法分析器知道如何过滤像空格,换行之类的无关字符。


图 6:从源文档到解析树(文档,词法分析,句法分析,解析树)。

解析过程是交互式的。解析器通常会从词法分析器获取新符号并尝试匹配句法规则。如果匹配成功,就在句法树上创建相应的节点,并继续从词法分析器获取下一个符号。如果没有匹配的规则,解析器会内部保存这个符号,并继续从词法分析器获取符号,直到内部保存的所有符号能够成功匹配一个规则。如果最终无法匹配,解析器会抛出异常。这意味着文档无效,含有句法错误。

转换

多数情况下解析树并非最终结果。解析经常是为了从输入文档转换成另外一种格式。比如编译器要把源码编译成机器码,会首先解析成解析树,再把解析树转换成机器码。


图 7:编译过程(源码,解析,解析树,转换,机器码)。

解析示例

在图5中我们构建了一个数学表达式解析树。让我们来试着定义一个简单的数学语言并看看解析是如何进行的。

词汇:我们的语言可以包含整数,加号和减号。

句法:

  1. 句法块由表达式,术语及操作符组成。
  2. 我们的语言可以包含任意数量表达式。
  3. 表达式定义为术语紧跟着操作符,再跟另外一个术语。
  4. 操作符是加号或减号。
  5. 术语可以是整数或表达式。

让我们分析输入”2 + 3 – 1″。

第一个符合规则的子字符串是”2″,根据规则#5它是一个术语。第二个匹配是”2 + 3″,符合第二条规则——一个术语紧跟一个操作符再跟另外一个术语。下一个匹配出现在输入结束时。”2 + 3 – 1″是一个表达式,因为我们已知“2+3”是一个术语,所以符合第二条规则。 “2 + + “不会匹配任何规则,所以是无效的输入。

词法与句法的合法性定义

词汇通常用 正则表达式来表示。

比如我们的语言可以定义为:

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

如你所见,整型是由正则表达式定义的。

句法常用 BNF格式定义,我们的语言被定义为:

expression :=  term  operation  term
operation :=  PLUS | MINUS
term := INTEGER | expression

我们说过常规解析器只能解析 上下文无关语法的语言。这种语言的一个直觉的定义是它的句法可以用BNF完整的表达。其规范定义请参考  http://en.wikipedia.org/wiki/Context-free_grammar

解析器的类型

解析器有两种基本类型——自上而下解析器和自下而上解析器。主观上可以认为自上而下的解析器从上层句法结构开始尝试匹配句法;自下而上的则从输入开始,慢慢转换成句法规则,从底层规则开始,直到上层规则全部匹配。

让我们看看这两种解析器将怎样解析我们的例子:

自上而下解析器从上层规则开始,它会把”2 + 3″定义为表达式,然后定义”2 + 3 – 1″为表达式(定义表达式的过程中也会匹配其它规则,但起点是最高级别规则)。

自下而上的解析器会扫描输入,直到有匹配的规则,它会把输入替换成规则。这样一直到输入结束。部分匹配的规则会放入解析堆栈。

Stack Input
2 + 3 – 1
term + 3 – 1
term operation 3 – 1
expression - 1
expression operation 1
expression

这种自下而上的解析器叫作移位归约解析器,因为输入被向右移动(想象一下一个指针从指向输入开始逐渐向右移动) 并逐渐归约到句法树。

自动创建解析器

有一些工具可以为你创建解析器,它们通常称为解析器生成器。你只需要提供语法——词汇与句法规则——它就能生成一个可以工作的解析器。创建解析器需要对解析器有深入的了解,并且手动创建一个优化的解析器并不容易,所以解析器生成工具很有用。

Webkit使用两款知名的解析器生成工具:Flex用于创建词法分析器,Bison用于创建解析器 (你也许会看到它们以Lex和Yacc的名字存在)。Flex的输入文件是符号的正则表达式定义,Bison的输入文件是BNF格式的句法定义。

HTML解析器

HTML解析器的工作是解析HTML标记到解析树。

HTML语法定义

HTML的词汇与句法定义在w3c组织创建的 规范中。当前版本是HTML4,HTML5的工作正在进行中。

不是上下文无关语法

在对解析器的介绍中看到,语法可以用类似BNF的格式规范地定义。不幸的是所有常规解析器的讨论都不适用于HTML(我提及它们并不是为了娱乐,它们可以用于解析CSS和JavaScript)。HTML无法用解析器所需的上下文无关的语法来定义。过去HTML格式规范由DTD (Document Type Definition)来定义,但它不是一个上下文无关语法。

HTML与XML相当接近。XML有许多可用的解析器。HTML还有一个XML变种叫XHTML,那么它们主要区别在哪里呢?区别在于HTML应用更加”宽容”,它容许你漏掉一些开始或结束标签等。它整个是一个“软”句法,不像XML那样严格死板。 总的来说这一看似细微的差别造成了两个不同的世界。一方面这使得HTML很流行,因为它包容你的错误,使网页作者的生活变得轻松。另一方面,它使编写语法格式变得困难。所以综合来说,HTML解析并不简单,现成的上下文相关解析器搞不定,XML解析器也不行。

HTML DTD

HTML的定义使用DTD文件。这种格式用来定义 SGML族语言,它包含对所有允许的元素的定义,包括它们的属性和层级关系。如我们前面所说,HTML DTD构不成上下文无关语法。

DTD有几种不同类型。严格模式完全尊守规范,但其它模式为了向前兼容可能包含对早期浏览器所用标签的支持。当前的严格模式DTD: http://www.w3.org/TR/html4/strict.dtd

DOM

解析器输出的树是由DOM元素和属性节点组成的。DOM的全称为:Document Object Model。它是HTML文档的对象化描述,也是HTML元素与外界(如Javascript)的接口。

DOM与标签几乎有着一一对应的关系,如下面的标签

<html>
	<body>
		<p>
			Hello World
		</p>
		<div> <img src="example.png"/></div>
	</body>
</html>

会被转换成如的DOM树:


Figure 8: DOM tree of the example markup

 

与HTML一样,DOM规范也由w3c组织制订。参考: http://www.w3.org/DOM/DOMTR. 这是一个操作文档的通用规范。有一个专门的模块定义HTML特有元素:  http://www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html.

当我们说树中包含DOM节点时,意思就是这个树是由实现了DOM接口的元素组成。这些实现包含了其它一些浏览器内部所需的属性。

解析算法

如我们前面看到的,HTML无法使用自上而下或自下而上的解析器来解析。

理由如下:

  1. 语言的宽容特点
  2. 浏览器需要对无效HTML提供容错性的事实。
  3. 解析过程的反复。通常解析过程中源码不会变化。但在HTML中,script标签包含”document.write”时可以添加内容,即解析过程实际上还会改变源码。

浏览器创建了自己的解析器来解析HTML文档。

HTML5规范里对解析算法有具体的说明,解析由两部分组成:分词与构建树。

分词属于词法分析部分,它把输入解析成符号序列。在HTML中符号就是开始标签,结束标签,属性名称和属生值。

分词器识别这些符号并将其送入树构建者,然后继续分析处理下一个符号,直到输入结束。


图 6: HTML解析流程 (源自HTML5规范)

 

分词算法

算法的输出是HTML符号。算法可以用状态机来描述。 每一个状态从输入流中消费一个或多个字符,并根据它们更新下一状态。决策受当前符号状态和树的构建状态影响。这意味着同样的字符可能会产生不同的结果,取决于当前的状态。算法太复杂,我们用一个例子来看看它的原理。

基础示例,分析下面的标签:

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

初始状态是”Data state”,当遇到”<”时状态改为 “Tag open state”。吃掉”a-z”字符组成的符号后产生了”Start tag token”,状态变更为 “Tag name state”。我们一直保持此状态,直到遇到”>”。每个字符都被追加到新的符号名上。在我们的例子中,解出的符号就是”html”。

当碰到”>”时,当前符号完成,状态改回 “Data state”。”<body>”标签将会以同样的方式处理。现在”html”与”body”标签都完成了,我们回到 “Data state”状态。吃掉”H”(”Hello world”第一个字母)时会产生一个字符符号,直到碰到”</body>”的”<”符号,我们就完成了一个字符符号”Hello world”。

现在我们回到 “Tag open state”状态。吃掉下一个输入”/”时会产生一个”end tag token”并变更为 “Tag name state”状态。同样,此状态保持到我们碰到”>”时。这时新标签符号完成,我们又回到 “Data state”。同样”</html>”也会被这样处理。


图 9: 示例输入源的分词处理

 

树的构建算法

当解析器被创建时,文档对象也被创建了。在树的构建过程中DOM树的根节点(Documen)将被修改,元素被添加到上面去。每个分词器完成的节点都会被树构建器处理。规范中定义了每一个符号与哪个DOM对象相关。除了把元素添加到DOM树外,它还会被添加到一个开放元素堆栈。这个堆栈用于纠正嵌套错误和标签未关闭错误。这个算法也用状态机描述,它的状态叫做”insertion modes”。

让我们看看下面输入的树构建过程:

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

树的构建过程中,输入就是分词过程中得到的符号序列。第一个模式叫 “initial mode”。接收 html 符号后会变成 “before html”模式并重新处理此模式中的符号。这会创建一个HTMLHtmlElement元素并追加到根文档节点。

然后状态改变为 “before head”。我们收到”body”时,会隐式创建一个HTMLHeadElement,尽管我们没有这个标签,它也会被创建并添加到树中。

现在我们进入 “in head”模式,然后是 “after head”,Body会被重新处理,创建HTMLBodyElement元素并插入,然后进入 “in body”模式。

字符符号”Hello world”收到后会创建一个”Text”节点,所有字符都被一一追加到上面。

收到body结束标签后进入  “after body” 模式,收到html结束标签后进入 “after after body”模式。所有符号处理完后将终止解析。


图 10: 示例HTML树的构建

解析结束后的动作

在这一阶段浏览器会把文档标记为交互模式,并开始解析deferred模式的script。”deferred”意味着脚本应该在文档解析完成后执行。脚本处理完成后将进入”complete”状态,”load”事件发生。

HTML5规范中包含了完整的算法: http://www.w3.org/TR/html5/syntax.html#html-parser

浏览器的容错

你永远不会看到HTML页面语法错误。浏览器会修正错误并继续。看看下面的例子:

<html>
  <mytag>
  </mytag>
  <div>
  <p>
  </div>
  	Really lousy HTML
  </p>
</html>

我一定违背了几百万条规则(”my tag”是非法标签,”p”与”div”元素嵌套错误等等),但浏览器仍然正确地显示,没有任何抱怨。所以很多解析器代码在修正这些HTML作者的错误。

浏览器的错误处理相当统一,惊人的是这并不是当前HTML规范的一部分,就像书签、前进、后退,只是多年以来在浏览器中开发出来的。有些无效的HTML结构出现在许多网站,浏览器会尝试用和其它各种浏览器一致的方式修复这些错误。

HTML5规范中应这一需求定义了一些东西,Webkit在它的HTML解析器类开头的注释中很好的做了摘要:

解析器分析输入符号生成文档,并构建文档树。如果文档格式良好,解析工作会很简单。
不幸的是,我们要处理很多格式不良的HTML文档,解析器需要宽容这些错误。
我们至少需要照顾下列错误:
1. 元素必需被插入在正确的位置。未关闭的标签应该一一关闭,直到可以添加新元素。
2. 不允许直接添加元素。
It could be that the person writing the document forgot some tag in between (or that the tag in between is optional).
This could be the case with the following tags: HTML HEAD BODY TBODY TR TD LI (did I forget any?).
3. We want to add a block element inside to an inline element. Close all inline elements up to the next higher block element.
4. If this doesn't help, close elements until we are allowed to add the element or ignore the tag.

Let’s see some Webkit error tolerance examples:

</br> instead of <br>

Some sites use </br> instead of <br>. In order to be compatible with IE and Firefox Webkit treats this like <br>.
The code:

if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
     reportError(MalformedBRError);
     t->beginTag = true;
}

Note – the error handling is internal – it won’t be presented to the user.

 

A stray table

A stray table is a table inside another table contents but not inside a table cell.
Like this example:

<table>
	<table>
		<tr><td>inner table</td></tr>
         </table>
	<tr><td>outer table</td></tr>
</table>

Webkit will change the hierarchy to two sibling tables:

<table>
	<tr><td>outer table</td></tr>
</table>
<table>
	<tr><td>inner table</td></tr>
 </table>

The code:

if (m_inStrayTableContent && localName == tableTag)
        popBlock(tableTag);

Webkit uses a stack for the current element contents – it will pop the inner table out of the outer table stack. The tables will now be siblings.

 

Nested form elements

In case the user puts a form inside another form, the second form is ignored.
The code:

if (!m_currentFormElement) {
        m_currentFormElement = new HTMLFormElement(formTag,    m_document);
}

 

A too deep tag hierarchy

The comment speaks for itself.

www.liceo.edu.mx is an example of a site that achieves a level of nesting of about 1500 tags, all from a bunch of <b>s.
We will only allow at most 20 nested tags of the same type before just ignoring them all together.
bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{

unsigned i = 0;
for (HTMLStackElem* curr = m_blockStack;
         i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
     curr = curr->next, i++) { }
return i != cMaxRedundantTagDepth;
}

 

Misplaced html or body end tags

Again – the comment speaks for itself.

Support for really broken html.
We never close the body tag, since some stupid web pages close it before the actual end of the doc.
Let's rely on the end() call to close things.
if (t->tagName == htmlTag || t->tagName == bodyTag )
        return;

So web authors beware – unless you want to appear as an example in a Webkit error tolerance code – write well formed HTML.

 

相关 [浏览器 工作 渲染引擎] 推荐:

浏览器是怎样工作的:渲染引擎,HTML解析(连载二)

- - 携程UED
渲染引擎的职责是……渲染,也就是把请求的内容显示到浏览器屏幕上. 默认情况下渲染引擎可以显示HTML,XML文档以及图片. 通过插件(浏览器扩展)它可以显示其它类型文档. 比如使用PDF viewer插件显示PDF文件. 我们会在一个专门的章节讨论插件与扩展. 在这一节我们将专注渲染引擎的主要用途——显示用CSS格式化的HTML与图片.

[转]手机浏览器工作原理

- - 小鸥的博客
现在电脑上的浏览器越来越多,出现了几大内核的浏览器纷争的局面. (小知识)几种常见的浏览器内核简介及优缺点:. Trident:Windows 下的 IE 浏览器使用的内核代号. 除 IE 外,众多的 IE Shell(如 Maxthon)都使用这个内核. 70%的占有率,几乎所有的互联网页面都要参照这个内核进行页面优化所以页面显示效果最好.

Blink: Google Chrome/Chromium 及 Opera 的新 Web 渲染引擎

- - LinuxTOY
Google 宣布将在未来的 Google Chrome/Chromium 中使用基于 WebKit 的 fork Web 渲染引擎:Blink. 同时 Opera 表示也将跟进 Google Chrome/Chromium 的步伐. Google Chrome/Chromium 从创始至今一直使用 WebKit(WebCore) 作为 HTML/CSS 渲染引擎.

为Canvas而生:开源2D HTML5渲染引擎CutJS

- - 博客园_新闻
CutJS 是一款专门用于跨平台游戏开发的开源 2D HTML5 渲染引擎,轻量级、快速、可交互,CutJS 的优势可谓颇多. CutJS 基于 MIT 许可协议发布,由 Piqnt 软件工作室推出,支持现代浏览器和移动设备,可用于 Web、iOS、Android、Win8、Facebook、Chrome Web Store 等平台游戏应用开发.

浅析渲染引擎与前端优化

- - JDC | 京东设计中心
浅析浏览器内核的工作原理(以. 浅析由浏览器内核想到的前端优化,或者说前端优化规则是从哪儿来的. 大家知道,大部分的 WEB 页面依托浏览器呈现,而浏览器能够将页面展示出来,基本依赖于浏览器的内核,即渲染引擎. 今天以 Chrome 浏览器的内核 WebKit(更确切是 WebKit 分支 Blink,以下统称为 WebKit )为例,对渲染引擎如何展示页面做个简单、全面的了解.

【快乐周末】浏览器是如何工作的?

- muqing - 谷奥——探寻谷歌的奥秘
今天这个漫画给我们详细的讲述了浏览器的工作过程,看着很累啊. © gkp 发表于 谷奥——探寻谷歌的奥秘 ( http://www.guao.hk ), 2010. | 16 条评论 | 永久链接 | 关于谷奥 | 投稿/爆料.

浏览器是怎样工作的(一):基础知识

- - 携程UED
前两天看到一篇不错的英文文章,叫做 How browsers work,该文概要的介绍了浏览器从头到尾的工作机制,包括HTML等的解析,DOM树的生成,节点与CSS的渲染等等,对于想学习浏览器源码的同学来说,实在是很棒的一篇科普文章. 尽管对于每部分,该文的描述并不足够深入,但综合来讲,即使没兴趣学浏览器源码,也还是有一些值的学习的思想在里面的,可以权当开拓下视野,有益无害.

web基础-web工作原理,http协议,浏览器缓存

- - 浏览器 - 互联网 - ITeye博客
4,cookie和session. 平时用浏览器,输入网址后回车,页面响应我们想要浏览的内容,简单操作的背后蕴涵了什么原理. 当输入url回车后,客户端(浏览器)会去请求DNS服务器,通过DNS获取域名对应的IP地址,然后通过这个地址找到对应的服务器,要求建立TCP连接,建立连接,客户端发送httpRequest(请求包)后,服务器接收并开始处理请求,调用自身服务,返回httpResponse(响应包),客户端收到响应包后开始渲染body主体,等到全部接收,断开与该服务器端的TCP连接.

Silk 浏览器:Google? No!

- 橙子 - 爱范儿 · Beats of Bits
前苹果员工, Blogger Chris Espinosa 指出, Amazon 的 Silk 浏览器技术,让 Amazon 不能把自己置于 Google 的控制之下. Silk 在云端为用户组织和优化网页,之后再下载到本地. 这样做的结果是, Amazon 能掌握用户在网络上的一举一动. 不仅仅包括在 Amazon.com 下的订单.

浏览器检测

- - JavaScript - Web前端 - ITeye博客
1.navigator 对象. 由于每个浏览器都具有自己独到的扩展, 所以在开发阶段来判断浏览器是一个非常重要的步骤. 虽然浏览器开发商在公共接口方面投入了很多精力, 努力的去支持最常用的公共功能;但在现实中,浏览器之间的差异,以及不同浏览器的“怪癖”却是非常多的,因此客户端检测除了是一种补救措施,更是一种行之有效的开发策略.