【译】浏览器如何工作:在现代web浏览器场景的之下

原文地址。

( 译者注:这是一篇深度好文,并且附带官方简体中文。本次的翻译为一人完成,限于水平,难免有误,故需要学习本文内容的同学请直接参考原文地址进行阅读。

导读: 终于,我在一周之内讲这长篇大论的浏览器背后的故事翻译完了。如果再要我重新阅读一遍,可能需要我沐浴焚香般的准备。现在我忍着肩膀和手腕的酸痛,写下发布前的最后一些体会:

  1. 这是一篇长度不小的文章。但是整个文章的内容可以说已经十分精炼,是一个以色列开发者在查阅数百万行 c++ 代码和无数文档的凝结之作。希望想更深入了解浏览器内部的读者们收藏原文。
  2. 尽管翻译结束了,但是我仍然需要好好消化一下文章内容。本来想挑出一部分关键内容供大家参考。但实在难以取舍。所以想要了解文章内容的同学,请快速阅览目录进行检索。
  3. 这篇文章,应该是我近一个月以来最长的一篇了。手酸,就写到这里吧。 )

序言

这是篇全面介绍 WebKit 和 Gecko 的内部操作的文章,它是以色列的开发者 Tail Garsiel 的大量的研究成果。过去几年,她重新审视了已公开的关于浏览器内部的资料(参考资料)同时花费了很多时间去阅读 web 浏览器源码。她写道:

在 IE 90% 支配的那个年代,把浏览器当做一个“黑盒”再也合适不过了,但是现在,开源浏览器占据了一半的市场份额,是时候去了解引擎的背后同时看看 web 浏览器的内部。尽管,里面有数百万行 C++ 代码……

Tail 在她的网站上发布了她的研究,但是我们想让更多的人知道,所以我们已经重新整理再次发布在这里。

作为一个 web 开发,了解浏览器操作的内部会帮助你做出更好的决定,同时在最佳开发实践时了解充分的理由。而这是一篇有长度的文章,我们推荐你花点时间去深挖探究,我们保证你会对自己的所做满意。 Paul lrish, Chrome 开发人员关系部

这篇文章被翻译成几种语言:HTML5 Rocks 翻译了 德语,西班牙语,日语,葡萄牙语,俄语和简体中文版本。你也可以看到韩语和土耳其语。 你也能看看关于这篇主题 Tail Garsiel 在 Vimeo 上的谈话。

(译者注:这篇目录翻译了我半个小时,通过目录的回顾确实跟之前的一些零碎知识串联了起来,发现很多。更主要的是,跑个题来缓解下被这个目录吓尿的心脏。)

目录

Web 浏览器是使用最广泛的软件。这篇读物中,我会解释在场景之后他们是如何工作的。我们将会看到,当你在地址栏输入 google.com 时直到在浏览器屏幕上看到 Google Page 页面后,发生了什么。

1.介绍

  • 目录
    • 1.介绍
      • 1.我们将要讨论的浏览器
      • 2.浏览器的主要功能
      • 3.浏览器的上层结构
    • 2.渲染引擎
      • 1.渲染引擎
      • 2.主要过程
      • 3.主要过程例子
    • 3.解析和 DOM 树结构
      • 1.一般解析
        • 1.语法
        • 2.解析器——词法分析器混合
        • 3.翻译
        • 4.解析举例
        • 5.变量和语法的定义格式
        • 6.解释器类型
        • 7.自动生成解析
      • 2.HTML 解析
        • 1.HTML 语法定义
        • 2.不是上下文无关文法
        • 3.HTML DTD
        • 4.DOM
        • 5.解析算法
        • 6.断词(标记)算法
        • 7.树构造器算法
        • 8.解析结束的行为
        • 9.浏览器容错度

          • 替换
          • 交叉表格
          • 嵌套元素
          • 深度标签层级
          • 错误放置的 html 或 body 结束标签
      • 3.CSS 解析
        • 1.WebKit CSS 解析
      • 4.脚本和样式表的执行顺序
        • 1.脚本
        • 2.推断解析
        • 3.样式表
    • 4.渲染树构造器
      • 1.渲染树和 DOM 树的关系
      • 2.树构建的过程
      • 3.样式计算
        • 1.共享样式数据
        • 2.火狐规则树
          • 1.结构分隔
          • 2.使用规则树计算样式上下文
        • 3.为简单匹配控制规则
        • 4.在正确的层叠顺序中应用规则
          • 1.样式表层叠规则
          • 2.明确性
          • 3.规则排序
      • 4.逐步过程
    • 5.布局
      • 1.Dirty 位系统
      • 2.全局和增量布局
      • 3.异步和同步布局
      • 4.优化
      • 5.布局过程
      • 6.宽度计算
      • 7.断行
    • 6.绘制
      • 1.全局和增量
      • 2.绘制顺序
      • 3.Firefox 展示列表
      • 4.WebKit 矩阵存储
    • 7.动态改变
    • 8.渲染引擎的线程
      • 1.事件循环
    • 9.CSS2 可视模型
      • 1.canvas
      • 2.CSS 盒模型
      • 3.定位方案
      • 4.盒子类型
      • 5.定位
        • 1.相对
        • 2.浮动
        • 3.绝对和固定
      • 6.表现层
    • 10.资料
      • 1.浏览器结构
      • 2.解析
      • 3.火狐
      • 4.WebKit
      • 5. W3C 规范
      • 6. 浏览器构建说明

1.我们将要讨论的浏览器

如今常用的主要浏览器有 5 种: Chrome,IE,火狐,Safari 和 Opera。在移动端上,主要的浏览器是安卓浏览器,苹果,Opera 迷你和 Opera移动端还有 UC 浏览器,诺基亚 S40/S60 浏览器和 Chrome 也都是,除了 Opera 浏览器,其他都是基于 WebKit。(译者注:前一句话在官方简体中文里没有.)我从开源浏览器火狐和 Chrome 以及 Safari(部分开源)中距离。根据 StatCounter 统计(从2013年6月以来) Chrome 火狐和 Safari 组成了全球桌面浏览器使用量的 71%。在移动端,安卓浏览器,iPhone 和 Chrome 有 54% 的使用率。

2.浏览器的主要功能

浏览器的主要功能是展示你选择的 web 资源,通过服务端的请求然后在浏览器窗口展示。这个资源通常是一个 HTML 文档,但也有可能是一个 PDF,一张图片,或者其他类型的内容。资源的位置通过用户使用 URI(Uniform Resource Identifier) 来明确指出。

浏览器插入和展示 HTML 文件的方式在 HTML 和 CSS 规范中有详细说明。这些规范通过 W3C(World Wide Web Consortium) 维护,这些规范也是 web 的标准组织。这些年的浏览器只是遵守一部分标准同时开发了他们自己的扩展。这对 web 开发者来说引发了一系列的兼容性问题。如今大多数浏览器或多或少遵守这些规范。

浏览器用户界面相互有很多共同之处。它们之间的共同元素有:

  • 用于插入 URL 的地址栏
  • 前进和后退按钮
  • 书签选择
  • 用于刷新和停止加载当前文档的刷新和停止按钮
  • 带你去主页的主页按钮

奇怪的是,浏览器的用户界面没有任何形式的规范,它只是从过去几年的经验和通过浏览器相互模仿中产生的最佳实践。 HTML5 规范没有定义一个浏览器必须拥有的 UI 元素,但是列出了一些常见元素。它们有地址栏,状态栏和工具栏。这些内容,尤其是,像火狐的下载管理器对具体浏览器而言是特有的。

3.浏览器的上层结构

浏览器的主要组件有(1.1):

  1. 用户界面:这包括地址栏,前进/后退按钮,书签菜单等等。每个部分除了你请求页面的的窗口都会显示。
  2. 浏览器引擎:在 UI 和渲染引擎之间的统一行为
  3. 渲染引擎:对展示请求的内容响应。比如请求的内容是 HTML,这个渲染引擎会解析 HTML 和 CSS,同时在屏幕上展示解析后的内容。
  4. 网络:对于网络调用比如 HTTP 请求,在独立的平台接口下对不同的平台使用不同的实现。
  5. UI 后台:用于绘制像组合盒子和窗口的基本组件。这个后台暴露的通用接口不是平台特有的。在这之下它使用了操作系统用户界面的方法。
  6. JavaScript 解释器:用于解释和执行 JavaScript 代码
  7. 数据存储:这是持续存在的一层。浏览器可能需要本地化存储数据的顺序,比如 cookies。刘安琪也支持 storage 机制比如 LocalStorage,IndexDB,WebSQL 和 文件系统。

图例:浏览器组件

对于浏览器而言这是很重要的,比如 Chrome 运行多个渲染引擎的实例为每一个标签。每个标签有个独立的进程。

2.渲染引擎

渲染引擎的责任是,额……渲染,也就是在浏览器屏幕上展示请求的内容。

默认的渲染引擎可以展示 HTML 和 XML 文档以及图片。它也可以通过插件或者扩展来展示其他的数据类型。举个例子,使用 PDF 视图插件展示 PDF 文档。然而,在本章节中我们将关注它的主要用处:展示使用 CSS 格式化的 HTML 和 图片。

1.渲染引擎

不同的浏览器使用不同的渲染引擎:IE 使用 Trident,火狐使用 Gecko,Safari 使用 WebKit。Chrome 和 Opera(自 15 版)使用 Blink,是Webkit 的一个分支。

WebKit是一个开源引擎,作为引擎,最开始在 Linux 平台上然后被 Apple 为了支持 Mac 和 Windows而修改。了解webkit.org的更多细节。

2.主要过程

渲染引擎从网络层开始获取请求文档的内容。这个通常在一个 8KB 块中完成。

在那之后,展示了渲染引擎的基本流程:

图例:渲染引擎基本工作流

渲染引擎开始解析 HTMl 文档同时在一个名叫“内容树”的树中转化元素变成 DOM 节点。引擎将会解析样式数据,外部的 CSS 文件和元素样式。在 HTML 中带有可视指令的样式信息将会被用于创建另一个树:渲染树。

渲染树包含了有可视属性像是颜色和尺寸的矩形。这个矩形在屏幕上以正确的顺序展示。

之后的渲染树会通过一个“布局”进程。这意味着该进程会给在屏幕上应该出现的每个节点一个精确的坐标。下一个阶段是绘制——渲染树被转换然后每个节点通过 UI 后台层被绘制。

理解这个渐进过程是非常必要的。为了更好地用户体验,渲染引擎会尽快尝试在屏幕上展示内容。它在开始构建和布局渲染树之前,不会等待所有的 HTMl 被解析。一部分内容被解析和展示,而进程继续剩余的从网络来的内容。

3.主要过程例子

图例:WebKit 主要流程

3.6

火狐的 Gecko 渲染引擎主要流程

从图 3 和图 4你可以看到,WebKit 和 Gecko 术语上有点不同,过程还是基本一样的。

Gecko 调用一个树,这个树是可视化的一个被格式化的“框架树”。每个元素是一个框架。WebKit 使用的叫做“Render Tree”,同时它由“Render Objects”组成。WebKit 对元素位置使用“Layout”,而 Gecko 称它为 “Reflow”。“Attachment”是WebKit一个术语,用于连接 DOM 节点和可视化信息用于创建渲染树。一个不重要的非语义的不同是 Gecko 在 HTML 和 DOM 树之间有一个额外的层,叫做 “content sink(内容沉淀)”,同时它是一个制作 DOM 元素的工场。我们将会讨论过程的每个部分:

3.解析和 DOM 树结构

1.一般解析

因为在渲染引擎中,解析是非常明显的进程,我们将会探索的深入一点。通过关于解析的简单介绍来开始。

解析一个文档意味着翻译为代码可用的结构。解析的结果通常是一个节点树,这颗树代表着文档结构。通常叫做解析树或者语法树。

举个例子,解析表达式 2 + 3 -1 可以返回下面的树:

图例:数学表达式的节点树

1.语法

解析基于文件遵守的语法规则:语言或者写入的格式。所有可以解析的格式必须由词汇和句法规则构成确定的语法。这称为上下文无关语法。人类语言不是这种语言,因此不能用常规的解析技术解析。

2.解析器——词法分析器混合

解析可以分为两个独立的子过程:词法分析和语法分析。

词法分析是将输入变成为标记的过程。标记是语言词汇:构建块的集合。在人类语言中它由所有在这个语言的字典中出现的单词构成。

语法分析是语言语法规则的应用。

解析通常在两个部分中独立的工作:词法分析器(有时也叫标记分析器),负责将输入变成有效地标记,同时解析器的责任是通过根据语言规则来分析文档构建解析树。词法分析器知道如何去除不相关的字符比如空格和换行。

图例:从源文档到解析树

解析过程是反复的。解析器通常为新的标记向词法分析器请求,并尝试将标记与某条语法规则匹配。如果规则匹配了,一个相应标记的节点将被添加到语法树中去,并且解析会请求下一个标记。

如果没有规则匹配,解析器会内部储存这个标记,并且保持请求标记直到一个匹配到所有储存在内部的标记的规则被发现。如果没有找到规则,那么解析器将抛出一个错误。这意味着文件无效并且包含语法错误。

3.翻译

在很多例子中,解析树不是最终产物。解析通常用于翻译:转换输入文档为另一种格式。举个例子比如编译:编译器编译源码成为机器码,首先编译成编译树,然后再编译成机器码文件。

图例:编译过程

4.解析举例

在图表5中,我们从数学表达式中构建编译树。我们试试定义一个简单的数学语言然后看看编译过程。

词汇:我们的语言包括数字,加减号。 语法:

  1. 语言语法组成了表达式,项和操作符。
  2. 语言包括任何数字表达式。
  3. 作为项的表达式通过链接另一个项的“操作符”链接。
  4. 操作符是加号或者减号标记。
  5. 项是数字或者表达式。

我们来分析输入的: 2 + 3 - 1

首先匹配到的规则串是 2:根据规则 #5 这是一个项。第二个匹配是 2 + 3:这个匹配了第三个规则:一个项链接着一个链接另一个项的操作符。下一个匹配将在输入的最后。2 + 3 -1 是一个表达式,因为我们知道, 2 + 3 是一个项,所以我们有一个项,通过链接另一个项的操作符链接着。2 + + 不会匹配任何规则,因此是一个无效的输入。

5.变量和语法的定义格式

词汇通常通过正则表达式表现。

举个例子,我们的语言将被定义为如下:

INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
复制代码

正如你看到的,整型通过正则表达式定义。

语法通常通过一种叫做 BNF 的格式定义。我们的语言将被定义为如下:

expression :=  term  operation  term
operation :=  PLUS | MINUS
term := INTEGER | expression
复制代码

我们认为,如果一种语言的语法是上下文无关的,那么它可以通过常规解析器解析。上下文无关语法的直观定义是可以在 BNF 中被完全表达的语法。一种形式的定义是关于上下文无关文法的维基百科的文章。

6.解释器类型

这里有两种解析类型:自顶向下和自底向上的解析。直观的解释是,自顶向下的解析检查上层语法结构然后尝试找到规则匹配。自底向上从输入开始,逐级转化成语法规则,从底层规则开始直到遇到顶层规则。

我们看看这两种解析类型如何解析我们的例子。

自顶向下解析从上层开始:它将识别 2 + 3为一个表达式。然后识别 2 + 3 - 1 为一个表达式(识别表达式的过程是逐步的,匹配其他规则的,但是是从上层开始。)

自底向上解析将会浏览输入直到一个规则被匹配。它将用这个规则替换匹配。这会一直执行直到输入的末端。部分匹配到的表达式在解析栈上被替换。

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

这种自底向上的解析称为移入规约解析,因为输入偏移到右侧(想象一个指针在输入开始然后移动到右侧),并且逐渐减少语法规则。

7.自动生成解析

有个工具可以生成解析。你告诉工具你的语言语法——它的词汇和语法规则——然后工具会生成一个有效解析。创建解析需要深刻理解解析,并且手动创建一个优化的解析并不容易,所以解析生成器是很有用的。

WebKit 使用两个著名的解析工具: Flex,用于创建词法,Bison,用于创建解析(你会或许把他们称为 Lex 和 Yacc)。Flex 输入是一个文件,包含标记的正则表达式定义。Bison 的输入是在 BNF 格式中的语言与法。

2.HTML 解析

HTML 解析器的工作是把 HTML 标记转化成解析树。

1.HTML 语法定义

HTML 的词汇和语法由 W3C 组织创造,在这个规范被定义。

2.不是上下文无关文法

如同我们在解析中的介绍,语法可以像 BNF 那样使用格式化的格式被定义。

不幸的是,所有常规的解析方式不能应用于 HTML(为了好玩我不把它们现在引入——它们在解析 CSS 和 JavaScript时将被用到)。HTML 不能像解析器需要的那样通过上下文无关文法被定义。

乍一看这个表现很奇怪; HTML 十分像 XML。有非常多的 XML 解析器可以使用。HTML 是 XML 的变体——所以有什么很大的不同吗?

这里的不同在于,HTML 尽可能的更多“包容”:它能让你省略某些标签(那些被隐式添加的),或者有时候省略开始或结束标签等等。整体来看它是“软性语法,与 XML 的严格硬性语法不同。

3.HTML DTD

HTML 定义在一种 DTD 格式里。这种格式用于定义 SGML 家族的语言。这个格式为所有允许的元素,它们的属性和等级定义。我们之前看到,HTML DTD 不是一种上下文无关文法。

DTD 有一些变体。严格模式适用于唯一的规范,但是其他模式包含对过去浏览器使用的标记的支持。这个目的是可以向后兼容老旧的内容。目前严格模式的 DTD 参考这里www.w3.org/TR/html4/st…。

4.DOM

输出树(解析树)是 DOM 元素和节点属性的树。DOM 是 Document Object Model 的缩写。它是 HTML 文档的表现对象和 HTML 元素对外部世界像是 JavaScript 元素的接口。树的根节点是 “Document” 对象。

DOM 对标记来说几乎要一对一的关系。举个例子:

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

标记将会被转化为下面的 DOM 树:

图例:例子中的 DOM 树

像 HTML,DOM 通过 W3C 组织定义。参考这里www.w3.org/DOM/DOMTR。它是对操作文档的一般定义。特殊的模型描述了 HTML 特殊元素。HTML 定义可以在这里找到:www.w3.org/TR/2003/REC…。

当我谈到树包含 DOM 节点时,我的意思是这棵树是有结构的元素,实现了 DOM 其中之一的接口。浏览器混合了这些实现,这些实现有一些通过浏览器内部定义的其他属性。

5.解析算法

如我们之前看到的部分一样,HTML 不能使用常规的自顶向下或者自底向上解析。

原因有:

  1. 语言的包容性
  2. 事实是,浏览器有错误容忍的传统,为了支持常见的无效的 HTML 的情况。
  3. 解析过程是不断重复的。对于其他语言,源码在解析的时候不会改变,但是 HTML,动态代码(比如包含 document.write() 的脚本元素调用)可以添加额外的标记,所以解析过程实际上修改了输入。

不能使用常规解析技术,浏览器为解析 HTML 创建了自定义解析。

由 HTML5 规范定义了解析算法的细节。算法有两个阶段组成:标记(断词)和结构树。

标记是词法分析,解析是输入变成标记。在 HTML 中,标记是开始标签,结束标签,属性名和属性值。

标记器识别标记,把标记给树构造器,并且为下个识别的标记处理下个字符,直到输入的结尾。

图例:HTML 解析过程(来自 HTML5 定义)

6.断词(标记)算法

这个算法的输出是 HTML 标记。这个算法被作为状态机表达。每个状态使用一个或者多个输入流的字符,并且根据这些字符更新下一个状态。这个决定通过当前标记状态和树构造状态影响。这就意味着消耗同样的字符为了正确的下个状态将会产出不同的结果,这取决于当前状态。这个算法过于复杂,以致不能完全描述,我们来看看一个简单的例子,这可以帮助我们理解这个规则。

基本例子:标记以下 HTML:

<html>
  <body>
    Hello world
  body>
html>
复制代码

初始化状态是 “Data State”。当遇到 < 字符时,状态变成“Tag open state”。使用 a-z 的字符产生“Start tag token”的创建,状态变为“Tag name state”。我们保留这个状态直到 > 字符出现。每个字符都被添加到新的标记名称上。在我们的例子中,这个创建的标记是 html 标记。

> 标签出现,当前标记被发送,同时状态变回 Data state 标签也是用相同的步骤处理。目前为止,htmlbody 标签被发送了。我们现在回到了 “Data state”。遇到 Hello world 字符的 H 将会引起创建和字符标记的发送,这将一直进行直到遇见 <。我们将为 Hello world 的每一个字符发送一个字符标记。

现在我们回到“Tag open state”。遇到下一个输入 / 将会引起结束标签的创建,并且移动到“Tag name state”。再一次我们保持在这个状态,直到我们遇见 >。此时这个新的标签标记将被发送,并且我们回到“Data state”。 输入将像之前的例子一样被处理。

标记案例输入

7.树构造器算法

当创建文档对象的解析器被创建。在树构造阶段期间,以 Document 为根节点的 DOM 树也被修改,并且元素被添加进去。通过标记生成器发送的每个节点将被树构造器处理。对于每个标记,规范定义了 DOM 元素与之相关,同时有一个开放元素的栈。这个栈用于正确嵌套没有匹配和没有闭合的标签。这个算法也是作为一个状态机描述。这个状态叫做“insertion modes”(插入模式)。

我们看看树构造过程输入的例子:

<html>
  <body>
    Hello world
  body>
html>
复制代码

从标记阶段中,输入给树构造器的阶段是连续的标记。初始模式是 “initial mode”。接收到 “html” 标记将会移动到 “before html” 模式,并且在那个模式中再处理标记。这将引发 HTMLHtmlElement 元素创建,它将被添加到 Document 对象的根节点中。

状态将被变为 “before head”。“body” 标记被接受时。HTMLHeadElement 将被隐式创建,尽管我们没有 “head” 标记,并且它会被添加到树中。

现在我们移动到 “in head” 模式,并且将会到 “after head”。body 标记是再次执行的,HTMLBodyElement 被创建和插入,模式被转换为 “in body”。

“Hello world” 字符串的字符标记现在接受了。首先会发生创建和 “Text” 模式的插入,并且其他字符也将加入到这个节点中。

body 结束标记引起到 “after body” 模式的转换。我们会收到 html 结束标记,它将会移动到 “after after body” 模式。接受文件标记的结束将会结束解析过程。

html 例子的树构造

8.解析结束的行为

在这个阶段,浏览器将会作为交互而标记文档,并且开始解析在 “deferred” 模式下的脚本:这些本应该在文档解析后被解析。文档状态将被设置为 “complete”然后一个 “load” 事件将被触发。

在这里能看到 HTML5 规范中标记器和树构造器的完整算法

9.浏览器容错度

你不会在 HTML 页面得到一个 “无效语法” 的错误。浏览器会修复任何无效的内容,然后继续。

比如下面的例子:

<html>
  <mytag>
  mytag>
  <div>
  <p>
  div>
    Really lousy HTML
  p>
html>
复制代码

我一定要违反很多规则(“mytag” 不是个标准标准标签,“p” 和 “div” 标签的错误嵌套等等)但是浏览器仍然正确展示,并且没有任何抱怨。以为很多解析代码在修复 HTML 作者的错误。

错误处理是十分一致的,但吃惊的是,它不是 HTML 规范的部分。如同书签和后退前进按钮一样,它只是这些年在浏览器中被发开出来。很多网站上有很多无效的 HTML 结构重复着,并且浏览器尝试用一种与其他浏览器一样的方式修复。

HTML 规范定义了一些要求。(WebKit 在 HTML 解析器类的开始的注释很好的总结了)

解析器解析标记输入成为文档,构建文档树。如果文档格式良好,解析会直接进行。

不幸的是,我们不得不处理很多 HTML 文档,那些文档没有很好的格式,所以解析器不得不容忍这些错误。

我们可以了解到至少以下几种错误条件:

1. 在某些标签外部,明确禁止添加元素。这种情况下,我们应该关闭所有的标签,直到一个禁止的标签出现,之后添加它。

2. 我们不允许直接添加元素。它可能是人们写入文档忘记的标签(或者其中的标签是可选的)。比如以下标签:HTML HEAD BODY TBODY TR TD LI(漏了什么吗?)

3. 我们想在行内元素中添加块元素。闭合所有的行内元素直到下一个更高级的块元素出现。

4. 如果这些都没有作用,直到允许我们添加或者忽略标签才会闭合元素。
复制代码

我们来看看 WebKit 的容错例子:


替换

有些网站使用
而不是
。为了兼容 IE 和 火狐, WebKit都当做

代码如下:

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

注意内部的错误处理:这不会展示给用户。

交叉表格

交叉表格是一个表格内部有另一个表格,但是不在单元格里。

比如:

<table>
    <table>
        <tr><td>inner tabletd>tr>
    table>
    <tr><td>outer tabletd>tr>
table>
复制代码

WebKit 将会改变两个子表的层级:

<table>
    <tr><td>outer tabletd>tr>
table>
<table>
    <tr><td>inner tabletd>tr>
table>
复制代码

代码如下:

if (m_inStrayTableContent && localName == tableTag)
        popBlock(tableTag);
复制代码

WebKit 为当前元素内容使用一个栈:它将弹出外部表格栈的内部表格。现在这个表格成了兄弟关系。

嵌套元素

在用户输入一个表单内部中包含另一个表单时,第二个表单将被忽略。

代码如下:

if (!m_currentFormElement) {
        m_currentFormElement = new HTMLFormElement(formTag,    m_document);
}
复制代码
深度标签层级

注释不言而喻。

www.liceo.edu.mx 是一个网站的例子,这个网站签到了大约 1500 个标签层级,所有这些来自 分支。在全部忽略它们之前,我们最多允许 20 个同类型的嵌套标签。

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;
}
复制代码
错误放置的 html 或 body 结束标签

再次参考注释:

为了支持被破坏的 HTML。我们永远不会闭合 body 标签,因为有些愚蠢的网站页面在文档真正结束之前闭合了它。我们依赖在 end() 调用上闭合它们。

if (t->tagName == htmlTag || t->tagName == bodyTag )
        return;
复制代码

所以 web 作者意识到——除非你想去表现一个 WebKit 容错代码片段作为例子——否则就写良好格式化的代码。

3.CSS 解析

还记得介绍里面的解析概念吗?好吧,像 HTML,CSS 是上下文无关语法,并且可以在介绍中使用解析类型定义类型定义来解析。事实上 CSS 规范定义了 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}*
复制代码

"ident" 是 identifier的 缩写,类似类名。“name” 是一个元素的 id(也被记作“#”)

语法在 BNF 中的描述:

ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;
selector
  : simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?
  ;
simple_selector
  : element_name [ HASH | class | attrib | pseudo ]*
  | [ HASH | class | attrib | pseudo ]+
  ;
class
  : '.' IDENT
  ;
element_name
  : IDENT | '*'
  ;
attrib
  : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
    [ IDENT | STRING ] S* ] ']'
  ;
pseudo
  : ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
  ;
复制代码

解释:一个规则集合如这样的结构:

div.error, a.error {
  color:red;
  font-weight:bold;
}
复制代码

div.errora.error 是选择器。花括号里面的部分包含这个规则,它们在规则集被应用。这个结构在定义中被形式地定义为如下:

ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;
复制代码

也就是说规则集是选择器或可选择地通过逗号和空格(S 代表空格)被一系列选择符分割。一个规则集包含花括号和内部的描述或者可选的逗号分割。

1.WebKit CSS 解析

WebKit 使用 Flex 和 Bison。如同从解析介绍中回忆起来的一样,Bison 创建了自底向上递归下降解析。火狐使用了自顶向下手工写入。这两种例子的每个 CSS 文件会被解析成一个 StyleSheet 对象。每个对象包含 CSS 规则。这个 CSS 规则对象包含选择器和对象声明以及其他对应 CSS 语法的对象。

图例:解析 CSS

4.脚本和样式表的执行顺序

1.脚本

web 的模型是同步的。创建者希望当解析器遇到

你可能感兴趣的:(【译】浏览器如何工作:在现代web浏览器场景的之下)