当前位置: 首页 > news >正文

【译】浏览器如何工作:在现代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 为一个表达式(识别表达式的过程是逐步的,匹配其他规则的,但是是从上层开始。)

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

StackInput
term2 + 3 -1
term operation+ 3 - 1
expression3 - 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<body> 标签也是用相同的步骤处理。目前为止,htmlbody 标签被发送了。我们现在回到了 “Data state”。遇到 Hello world 字符的 H 将会引起创建和字符标记的发送,这将一直进行直到遇见 </body><。我们将为 Hello world 的每一个字符发送一个字符标记。

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

标记案例输入

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 的容错例子:


替换

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

代码如下:

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

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

交叉表格

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

比如:

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

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

<table>
    <tr><td>outer table</td></tr>
</table>
<table>
    <tr><td>inner table</td></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 个标签层级,所有这些来自 <b> 分支。在全部忽略它们之前,我们最多允许 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 的模型是同步的。创建者希望当解析器遇到 <script> 标签被解析后立即执行。文档解析器中止直到脚本被执行。如果是外部脚本,那么资源首先必须从网络请求——这也是同步处理的,同时直到资源获得,否则解析一直中止。这个模型有许多年了,同时在 HTML4 和 HTML5 中被定义。创作者可以给脚本添加 “defer” 属性,在这种情况下,这将不会终止文档解析,并且在文档解析后执行脚本。HTML5添加一个可选标记给脚本作为异步标记,以便将来解析和通过不通线程执行。

2.推断解析

Webkit 和 Firefox 都做了这种优化。当脚本执行时,另一个线程解析剩余的文档,并且找出从网络上需要加载的其他资源然后加载它们。用这种方式,资源可以在平行连接上加载,并且总的来说速度是提升的。注意:推断解析只解析外部资源像是外部脚本,样式表和图片:它不会修改 DOM 树——这留给主要解析器。

3.样式表

另一方面样式表有着不同的模型。概念上来说因为它看起来并不修改 DOM 树,所以没有理由去等待他们和停止文档解析。这里有个问题,即在文档解析阶段,为样式信息的脚本请求。如果样式没有加载和解析,脚本将会得到错误答案,并且显然这会引起一系列问题。这看起来是个边缘问题,但事实上很常见。当有样式表仍然在加载和解析的时候,火狐阻止了所有的脚本。WebKit 只会阻止当尝试去访问某些样式属性,而这些属性可能被未加载的样式影响的脚本。

4.渲染树构造器

当 DOM 树被构建时,浏览器构建另一个树,是渲染树。这是棵可视元素按照展示顺序排列的树,是可视化文档的表现。这棵树的目的是可以在它们正确的顺序下绘制内容。

火狐在渲染树的 “frames” 中调用元素。 WebKit 使用渲染项或者渲染对象。

渲染知道如何布局和绘制它自己以及它的后代。

WebKit的 RenderObject 类,渲染的基础类,有如下定义:

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 代码决定了哪种渲染类型应该创建为一个 DOM 节点,根据 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中,如果元素想去创建特殊渲染,它会覆写 createRender() 方法。渲染指向样式对象,这个对象不包含几何信息。

1.渲染树和 DOM 树的关系

渲染对应着 DOM 元素,但并不是一对一的干洗。不可见的 DOM 元素不会插入到渲染树中。举个例子 “head” 元素。那些 display 值是 “none” 的元素也不会出现在树中(但是 visibility 是 “hidden” 的元素会出现)。

DOM 元素对应一些可视对象。常见的元素带有复杂的结构,它们不能通过一个矩形描述。比如: “Select” 元素有三个渲染:一个用于展示区域,一个用于下拉列表,还有一个用于按钮。当文字换行时也一样,因为宽度不满足一行,新的行必须作为额外的渲染添加。

多行渲染的另一个例子是破坏的 HTML。根据 CSS 定义,内联元素要么只包含块元素要么只包含内联元素。在混合内容的例子中,匿名块将被创建用于包含内联元素。

一些渲染对象对应 DOM 节点,但是不在树中同样的位置上。浮动和绝对定位元素不在流上,被放在了树的不同部分,映射在真实的结构上。一个占位结构在它们应该在的位置上。

图例:渲染树和对应的节点树。“viewport” 是初始化包含块。在 WebKit 中它是 “RenderView” 对象。

2.树构建的过程

在火狐中,表现层被当做监听注册在 DOM 更新上。表现层给 FrameConstructor 委托框架创建,并且构造器解析样式(参考样式计算)和创建框架。

在 WebKit 处理样式和创建渲染层的过程叫做 “attachment”。每个 DOM 节点都有一个 “attach” 方法。Attachment 是同步的,节点插入给 DOM 树调用新节点的 “attach” 方法。

处理 html 和 body 标签的结果是渲染树根节点的构造。根渲染对象对应 CSS 中叫做包含块的规范:最顶部的包含其他所有块的块。它的尺寸是视窗:即浏览器窗口的区域尺寸。火狐称作 ViewPortFram 而 WebKit 称作 RenderView。这是文档指向的渲染对象。其余的树作为 DOM 节点插入被构造。

参考 CSS2 规范的过程模型

3.样式计算

构建渲染树需要计算每一个渲染对象的可视属性。这通过计算每个元素的样式属性来完成。

样式包括各种来源的样式表,内联样式元素和 HTML 中的可视化属性(像是 “bgcolor” 属性)。之后被翻译成匹配 CSS 样式属性。

样式表的来源有,浏览器默认样式,网页创作者提供的样式,和用户样式——这些样式表通过浏览器使用者提供(浏览器允许你定义自己喜欢的样式。在火狐中,初始化,通过放在 “Firefox Profile”文件夹中的样式完成)。

样式计算有点困难:

  1. 样式数据是很大的结构,控制非常多的样式属性,这可能引起内存问题。
  2. 为每个元素查找匹配规则可能引起性能问题,在没有优化的情况下。遍历每个元素的全部规则去找到匹配的内容是一件繁中的任务。选择器有复杂的结构,这就导致佩佩过程看起来是有效地路径,其实是无效的,然后不得不去尝试另一条路径。 举个例子——这个混合的选择器:
    div div div div {}
    复制代码
    意味着这个规则会应用于 3 个 <div> 的后代。假设你想去检查是否这个规则应用于一个 <div> 元素。你可以选择某条树上向上路径去检查。你也可以传过节点树向上发现只有两个 div,然后规则并不适用。你这是需要去尝试另一颗树。
  3. 应用规则包含了十分复杂的层叠规则,这个规则定义了规则的等级。

我们看看浏览器如何面对这些问题:

1.共享样式数据

WebKit 节点引用样式对象(RenderStyle)。这些对象可以通过节点在相同的条件下共享。这些节点是兄弟或者表兄弟并且:

  1. 元素必须在相同的鼠标状态(比如:不能一个状态是 :hover 而另一个不是)
  2. 任何元素不能没有 id
  3. 标签名应该匹配
  4. 类属性应该匹配
  5. 映射属性的集合完全相同
  6. 链接状态必须匹配
  7. 焦点状态必须匹配
  8. 任何元素应该不被属性选择器影响,这里的影响定义为在任何位置使用了选择器的,使用属性选择器的任何选择器匹配。
  9. 元素上不应该有内联样式属性。
  10. 不应该有兄弟节点匹配。WebCore 简单地引发一个全局开关,当任何兄弟选择器相遇时,并且当他们存在时时对全部文档禁用样式共享。这包括 + 选择器和像 :first-child 和 :last-child 选择器。

2.火狐规则树

火狐有两颗额外的树用于简化样式计算:一颗规则树和样式上下文树。WebKit 也有样式对象,但是没有像样式上下文树来存储,只有 DOM 节点指向相关样式。

图例:火狐样式上下文树

样式上下文包含结束值。这个值被应用在所有正确顺序下的匹配规则和实行从逻辑到实际值的转换操作而计算。举个例子,如果逻辑值是屏幕上的百分比,它将被计算和转换成绝对单位。这个规则树的注意很聪明。它可以在节点中分享这些值,避免重复计算。这也节约了空间。

所有的匹配规则储存在一棵树中。路径上的底部节点有更高的优先级。树包含所有的路径,为了匹配已经发现的规则。存储这些规则是懒处理的。树不会在每个节点开始的时候计算,但无论何时一个节点样式需要计算时,计算路径被添加到树中。

这个点子看树像是在词法中看单词。我们看看已经计算的规则树:

假设我们需要为上下文树的其他元素匹配规则,并且找出匹配规则(正确的顺序)是 B-E-I。我们已经有在树中的路径,因为我们已经计算出了路径 A-B-E-I-L。我们将减少我们的工作。

来看看树如何节约我们的工作。

1.结构分隔

样式内容被分割成结构。这些结构包含了样式信息,像是边框和颜色这种种类。结构中的所有属性是继承或者不继承的。除非元素定义了继承属性,否则从父级继承。如果没有定义继承属性,使用默认值。

2.使用规则树计算样式上下文

当为某个元素计算样式上下文时,我们首先计算规则树中的路径或者使用已经存在的。接着我们开始在路径中应用规则去在我们新的样式上下文中应用规则。我们开始从路径底部节点——这个节点由最高的优先级(通常是最特殊的选择器)和穿过树到顶部直到我们的结构被填满。如果在规则节点的结构上没有定义,我们可以很好地优化——我们到树上直到我们发现一个节点,是充分定义和简单指向它——这是最好的优化——真个结构被共享。这节约了末尾值的计算和内存。

如果我们发现部分定义,我们到树上填满。

如果在结构上找不到任何定义,有些例子中结构是 “继承” 类型,我们在上下文树中指向我们父级的结构。在有些例子中我们也成功共享了结构。如果默认值被使用它就是重置结构。

如果大部分节点没有添加值,那么我们需要做一些额外的计算,为它转换成实际值。我们接着在树节点中缓存结果为了让后代使用。

此例中元素有一个兄弟节点,它指向同一个树节点,那么全部样式上下文可以在它们之间共享。

假定我们有如下 HTML:

<html>
  <body>
    <div class="err" id="div1">
      <p>
        this is a <span class="big"> big error </span>
        this is also a
        <span class="big"> very  big  error</span> error
      </p>
    </div>
    <div class="err" id="div2">another error</div>
  </body>
</html>
复制代码

和以下规则:

div {margin:5px;color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}
复制代码

为了简化这些事我们需要只填满这两种结构:颜色结构和边距结构。颜色结构包括只包括一个成员:颜色。边距结构包含四个方面。

这个结果规则树看起来是这样(节点被节点名称标记:它们指向规则的数量):

图例:规则树

上下文树将看起来像这样(节点名:它们指向规则节点):

图例:上下文树

假设我们解析 HTML 并且得到第二个 <div> 标签。我们需要为这个节点创建样式上下文和填充它的样式结构。

我们匹配这些规则并且找出 <div> 匹配规则是 1,2 和 6的。这意味着在树中已经有存在的路径,我们的元素可以使用这些路径,并且我们为规则 6 只需要添加另一个节点给它(在规则树中的节点 F)。

我们将创建规则上下文并且在上下文树中放置。新的上下文内容将会在规则树中指向节点 F。

我们需要填充样式结构。我们从填满边距结构开始。因为最后一个规则节点(F)没有添加到边距结构中,我们可以在树上直到找到在之前节点插入的缓存结构然后使用它。我们将发现它在节点 B 上,它是定义的边距规则的最高节点。

在第二个 <span> 元素上的工作相对容易。我们匹配到规则然后得出指向规则 G 的结论,像是之前的 span。因为我们有一个兄弟节点指向相同的节点,我们可以共享全部样式上下文,然后指向之前 span 的上下文。

对于集成自父级的包含规则的结构,缓存在上下文树中被处理(颜色属性实际上是继承,但是火狐当做默认对待然后缓存在规则树上)。

如果我们在段落里为字体添加规则作为实例:

p {font-family: Verdana; font size: 10px; font-weight: bold}
复制代码

接着这个段落元素,它是在上下文树的div的子代,作为它的父级它可以共享相同字体。这是在段落中没有字体规则定义的情况。

在 WebKit 中,那些没有规则树的,匹配声明会转化四次。首先非重要高级优先权属性被应用(属性应该是第一次应用因为其他依赖它们,比如 display),接着高优先级重要的,接着是常规优先级不重要的,最后是常规优先级重要规则。这意味着根据正确的层叠规则属性出现四次。最后的获胜。

来总结是:共享样式对象(全部和部分的内部结构)处理问题在 1 和 3。火狐规则树也会帮助在正确的顺序下应用规则。

3.为简单匹配控制规则

这里有几个样式规则的来源:

  • CSS 规则,既不是外部样式也不是元素中样式。
    p {color: blue}
    复制代码
  • 内联元素属性如下:
    <p style="color: blue" />
    复制代码
  • HTML 可视属性(这映射到相关样式规则)
    <p bgcolor="blue" />
    复制代码

最后两个对元素来说是简单匹配,因为它自身的样式属性和 HTML 属性可以作为 key 映射到使用的元素。

注意之前的问题 2,CSS 规则匹配比较难办。为了解决这个困难,这个规则为了更容易访问需要手动操作。

在解析样式表之后,规则根据选择器被添加到一个哈希表上。这个表通过 id,类名,标签名和通常不属于上述类别的规则来映射。如果选择器是 id,规则被添加到 id 映射,如果是类,被添加到类映射等等。

这种控制使得匹配规则变得简单。不需要去检查每一处声明:我们可以为元素从映射中取出相关的规则。这优化排除了 95% 的规则,所以在匹配过程中甚至可以不需要考虑(4.1)。

以如下样式规则为例:

p.error {color: red}
#messageDiv {height: 50px}
div {margin: 5px}
复制代码

第一条规则将被插入到类映射中。第二条规则插入 id 映射,然后第三条插入标签映射。

参考下列 HTML 片段:

<p class="error">an error occurred </p>
<div id=" messageDiv">this is a message</div>
复制代码

我们首先尝试为 p 元素找到规则。类表中有一个 “error” 键,在下面会找到 “p.error” 的规则。div 元素在 id 表和标签表中找到相关规则。剩下的工作只是找出哪些根据键提取的规则是真正的匹配了。

比如 div 规则如下:

table div {margin: 5px}
复制代码

它会从类表中提取,因为键是最右边的选择器,但是它不会匹配我们的 div 元素,它没有 table 祖先。

WebKit 和 火狐都做了这种操作。

4.在正确的层叠顺序中应用规则

样式对象有对应每一个可视化属性(所有的 CSS 属性但更通用)的属性。如果一个属性没有被任何匹配的规则定义,这些属性可以通过父级元素样式对象来继承。其他属性使用默认值。

当有更多的定义时问题就来了——这时候需要层叠顺序来解决这个问题。

1.样式表层叠规则

样式属性的声明可能会出现在多个样式表中,或者在一个样式表中声明数次。这意味着应用工作的顺序是很重要的。这叫做 “层叠” 顺序。根据 CSS2 定义,层叠顺序是(从低到高):

  1. 浏览器声明
  2. 用户一般声明
  3. 创作者一般声明
  4. 创作者重要声明
  5. 用户重要声明

浏览器声明是不重要的,当用户只有把声明标记为 important 时才会覆盖创作者的内容。同样顺序的声明会根据定义来排序,然后在根据指定的顺序。HTML 可视属性被转换成匹配 CSS 声明。它们被当做低权限的创作者规则。

2.明确性

选择器的明确性通过如下 CSS2 规范来定义:

  • 如果声明来自样式属性而不是选择器属性(=a),计为 1
  • 计算选择器中 ID 属性的数量(=b)
  • 计算选择器中其他属性和伪类的数量(=c)
  • 计算选择器中元素名和伪元素的数量(=d)

连接这四个数字 a-b-c-d(在大基数进制的数字系统),得到明确性。

基于你需要使用的进制取决于在某个类目中定义的最多的数量。

举个例子,如果 a = 14 你需要使用 16 进制。不太可能的情况是 a = 17 的时候你需要使用 17 进制。后一种场景更像是这样的选择器:html body div div p ... (17 个元素在选择器中……不是很有可能)

举个例子:

 *             {}  /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
 li            {}  /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
 li:first-line {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul li         {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
 h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
 ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
 li.red.level  {}  /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
 #x34y         {}  /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
 style=""          /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */
复制代码
3.规则排序

在匹配规则以后,根据层叠规则排序。WebKit 使用冒泡排序为小型列表然后合并成一个大的。WebKit 为规则覆写 “>” 操作实现排序。

static bool operator >(CSSRuleData& r1, CSSRuleData& r2)
{
    int spec1 = r1.selector()->specificity();
    int spec2 = r2.selector()->specificity();
    return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2;
}
复制代码

4.逐步过程

WebKit 使用一个标记,记录所有最高层加样式表(包括 @imports)是否加载完毕。如果在 attaching 中没有完全加载,使用占位符并且在文档中标记,然后一旦样式表被夹在会重新计算。

5.布局

当渲染被创建和添加到树中后,它还没有位置和尺寸。计算这些值的过程叫做布局和重绘(reflow)。

HTML 使用的流基于布局模型,意味着在大多数情况下一次计算就可以得到几何值。流之后的元素一般不会影响流之前的元素的几何性质,所以通过文档布局可以从左只有,从上到下的执行。有个例外:比如,HTML 表格可能会要求多次遍历(3.5)

坐标系统是相对于根框架的。使用上左位置的坐标。

布局是个递归过程。它在根节点渲染开始,也就是对应 HTML 文档的 <html> 元素。布局通过一些或者全部框架层级,为每次要求的渲染计算几何信息。

根渲染的位置是 0,0 并且它的尺寸是浏览器窗口的可见部分的视窗。

所有的渲染都有 “layout” 和 “reflow” 方法,每个渲染调用后代需要布局的布局方法。

1.Dirty 位系统

为了对每一次小改变不做充分的布局,浏览器使用 “dirty bit” 系统。改变或者给它自己和后代添加标记的渲染视为 “dirty”:需要布局。

有两种标记:“dirty” 和 “children are dirty”, 这意味着尽管渲染自己是合适的,它至少有一个子代需要布局。

2.全局和增量布局

布局在整个渲染树上能被触发——这是“全局”布局。它能作为以下结果发生:

  1. 影响所有渲染的全局样式,像是字体大小改变。
  2. 屏幕改变的结果。

布局是增量的,只有脏渲染会布局(这回引起一些额外布局的损失)。

增量布局当渲染是脏的时候触发(异步的)。举个例子,在从网络的额外内容被添加到 DOM 树后新的渲染将被添加到渲染树。

增量布局——只有脏渲染和它的后代布局

3.异步和同步布局

增量布局是通过异步完成的。火狐为增量布局将 “回流命令”排队,同时调度者会触发这些命令批量执行,然后“脏”渲染被布局。

脚本请求了样式顺序,像是“offsetHeight”可以同步地触发增量布局。

全局布局通常被同步触发。

有时因为一些属性,比如滚动位置改变,布局会在初始化布局之后作为回调触发。

4.优化

优化当布局被 “resize” 触发时,或者渲染位置改变(不是尺寸),渲染尺寸从缓存中获取并且不会重新计算。

在一些例子中只有一个子树被修改,并且布局没有从根节点开始的话。这地方只会发生本地改变不会影响它的周围——比如文本被插入进文本域(否则每次敲击将会从根节点触发布局)。

5.布局过程

布局通常有以下默认:

  1. 父级渲染决定自身宽度
  2. 父级依次处理后代:
    1. 放置子代渲染(设置 x 和 y)
    2. 调用子代布局,如果需要的话——它们是脏布局或者我们在在全局中,或者其他的原因——这会计算子代高度
  3. 父级使用子代积累的高度和边距的高度,然后补充自己的高度——这会通过父级渲染的父级使用。
  4. 设置脏位值为 false

火狐使用一个 “state” 对象(nxHTMLReflowState)作为布局参数(记为“reflow”)。在它们之间这个状态包括父级宽度。火狐的布局输出是 “metrics” 对象(nsHTMLReflowMetrics)。它将包含渲染计算的高度。

6.宽度计算

渲染器的宽度使用包含块的宽度来计算,渲染样式 “width” 属性是 margin 和 border。

比如下面这个 div 的宽度:

<div style="width: 30%"/>
复制代码

通过 WebKit 计算可能如下(RenderBox 类 的 calcWidth 方法):

  • 包含的宽度是包含块可用宽度的最大值或 0.这个可以用宽度在例子中是这样被计算的内容宽度:

    clientWidth() - paddingLeft() - paddingRight()
    复制代码

    客户端宽度和客户端高度代表一个包括边距和滚动调的对象的内部

  • 元素宽度是样式属性 “width”。它可以计算成绝对值,通过计算容器宽度的百分比。

  • 水平边框和补白被添加。

目前这种这是“完美宽度”的计算。现在最小和最大宽度被计算。

如果最佳宽度比最大宽度更大,这个最大宽度将被使用。如果小于最小宽度(最小的不可破坏的单位)那么最小宽度被使用。

只被缓存以防布局使用,但是宽度不会改变。

7.断行

当渲染到布局的中间决定它需要换行时,渲染停止然后扩散需要换行布局的父级。父级创建额外的渲染,然后在上面调用渲染。

6.绘制

在绘制阶段,渲染被传递,并且渲染的 “paint()” 方法被调用用于在屏幕上展示内容。绘制使用 UI 基础组件。

1.全局和增量

如同布局,绘制也是全局的——整棵树被绘制——或者增加。在增加绘制中,一些渲染在不影响整颗树的情况下改变。这个改变的渲染在屏幕上使它的矩形失效。这是因为操作系统把它当做一块“脏区域”,同时生成了“绘制”事件。操作系统聪明的合并几个区域变成一个。在 Chrome 中,这比较复杂因为渲染在主过程中有不同的过程。Chrome 某些程度模拟了操作系统的行为。表现层监听了事件并且代理渲染根部的消息。树被传递直到相关渲染到达。它将重绘自己(和通常它的子代)。

2.绘制顺序

CSS2 定义了绘制过程的顺序。实际上这个顺序是元素在内容栈的存储的地方。因为栈渲染从后向前,所以这个顺序影响绘制。一个块的栈顺序渲染是:

  1. 背景色
  2. 背景图片
  3. 边框
  4. 子代
  5. 外部描线

3.Firefox 展示列表

火狐遍历渲染树,然后为绘制矩形构建展示列表。它包含渲染层相关的矩形,在正确的绘制顺序下(渲染层的背景和边框等等)。这种方式树为一次重绘只会传递一次而不是数次——绘制所有的背景和图片,然后是边框等等。

火狐通过不添加将被隐藏的元素优化过程,像是元素完全在其他不透明元素下方。

4.WebKit 矩阵存储

在重绘前,WebKit 储存旧的矩形作为位图。这样只会渲染在新旧矩形之间的变化。

7.动态改变

在响应变化时,浏览器尝试最小化的可能的行为。所以改变一个元素的颜色只会引起元素重绘。改变元素的位置会引起元素和它的子代或可能的兄弟节点的布局和重绘。添加一个节点将引起节点的布局和重绘。主要的变化,像是增加 “html” 元素的字号,将会引起缓存失效,整个树的重布局和重绘制。

8.渲染引擎的线程

渲染引擎是单线程的。几乎所有的事,除了网络操作,都发生在单线程中。在火狐和 Safari 中这是浏览器的主线程。在 Chrome 中,tab 过程是主线程。

网络操作可以通过几个平行线程执行。平行链接数是受限的(2-6 个链接)。

1.事件循环

浏览器主要线程是时间循环。这是一个保持过程活动的无限循环。它等待事件(像是布局和绘制事件)然后执行它们。下面是火狐代码的主要事件循环:

while (!mExiting)
    NS_ProcessNextEvent(thread);
复制代码

9.CSS2 可视模型

1.canvas

根据 CSS2 定义,canvas 条款描述 “格式化结构渲染的空间”:是浏览器绘制内容的地方。canvas 对每个空间的尺寸是无限的,但是浏览器基于视窗的尺寸选择一个初始化的宽度。

根据 www.w3.org/TR/CSS2/zin…,canvas 是透明的,如果包含其他内容,然而浏览器定义一个颜色如果它没有定义的话。

2.CSS 盒模型

CSS 盒模型描述了一个矩形盒子,它在文档中为元素生成,同时根据可视化格式模型布局。

每个盒子有一个内容面积(比如文本和图片等等),同时可选有间距,边框,和边距面积。

图例:CSS2 盒模型

每个节点生成 0 到 n 和盒子。

所有的元素都有 “display” 属性,来定义将被生成的盒子类型。比如:

block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
复制代码

默认是 inline,但是浏览器样式表可能设置其他默认。举个例子:“div” 元素的默认展示是 “block”。

你可以在这里查看默认样式表的例子:www.w3.org/TR/CSS2/sam…

3.定位方案

有三种方案:

  1. 正常:对象根据文档中的位置被放置。意味着它在渲染树中的位置像在 DOM 树中的位置,根据盒模型和尺寸来布局。
  2. 浮动:对象像首先像正常流一样布局,然后尽可能移动到最最左或者最右。
  3. 绝对定位:对象放置在渲染树中,跟 DOM 树的位置不同。

定位方案通过设置 “position” 属性和 “float” 属性。

  • 静态和相对定位引发正常流
  • 绝对和固定定位引发绝对定位

在静态定位中,没有位置被定义,并且使用默认位置。在其他方案中,创作者定义位置用:上下左右。

盒子布局的方式取决于:

  • 盒子类型
  • 盒子尺寸
  • 位置方案
  • 外部信息比如图片大小和屏幕尺寸

4.盒子类型

块状盒子:在浏览器窗口中有自己的矩形的一种盒子形式。

图例:块盒子

内联盒子:没有自己的块,但是内部有内容块。

图例:内联盒子

块是一个接一个的格式化垂直。内联是格式化水平。

图例:块和内联格式化

内联盒子内部有行或者 “line boxes”。行至少与最高的盒子一样高而且可以比它更高,当盒子对齐在 “baseline”时——意味着底部元素部分对齐另一个元素的底部。如果容器宽度不够,内联会换行。这个通常发生在段落中。

5.定位

1.相对

相对定位-像通常一样定位,然后根据变化移动。

图例:相对定位

2.浮动

一个浮动盒子漂移到行的左侧或者右侧。这个有趣的特性会让其他盒子围绕着它。

<p>
  <img style="float: right" src="images/image.gif" width="100" height="100">
  Lorem ipsum dolor sit amet, consectetuer...
</p>
复制代码

看起来像这样:

图例:浮动

3.绝对和固定

布局精确定义忽略正常流。元素不参与正常流。这个尺寸相对于容器。在固定定位中,容器是视窗。

图例:固定位置

注意:固定盒子在文档滚动的时候不会移动。

6.表现层

这个通过 CSS 的 z-index 属性定义。它代表沿 “z轴” 的第三维度。

盒子被分割成“栈”(称作栈内容)。每个栈后面的元素将会先画在元素顶部,更接近用户。在重叠的例子中,最前的元素将会隐藏较前的元素。

栈根据 z-index 属性排序。从本地栈中盒子有 ‘z-index’ 属性。视窗在最外部栈。

比如:

<style type="text/css">
      div {
        position: absolute;
        left: 2in;
        top: 2in;
      }
</style>

<p>
    <div
         style="z-index: 3;background-color:red; width: 1in; height: 1in; ">
    </div>
    <div
         style="z-index: 1;background-color:green;width: 2in; height: 2in;">
    </div>
 </p>
复制代码

结果是:

图例:固定定位

尽管红色 div 在构建上高于绿色的,它可能在常规流之前,它的 z-index 属性 更高,所以在根盒子持有的栈中更向前。

10.资料

1.浏览器结构

  1. Grosskurth, Alan. Web 浏览器的引用结构
  2. Gupta, Vineet. 浏览器如何工作——第一部分——结构

2.解析

  1. Aho, Sethi, Ullman, 编译:规则,技术和工具(又叫做 “龙书”)Addison-Wesley, 1986
  2. Rick Jelliffe. 粗体之美:HTML 5 的两种草案

3.火狐

    1. L. David Baron, HTML 和 CSS 特性:写给 Web 开发者的布局引擎的内部.
  1. L. David Baron, HTML 和 CSS 特性:写给 Web 开发者的布局引擎的内部
  2. L. David Baron, Mozilla 的布局引擎
  3. L. David Baron Mozilla 样式系统文档
  4. Chris Waterson, HTML 回流笔记
  5. Chris Waterson, Geoko 概述
  6. Alexander Larsson,HTML HTTP 请求周期

4.WebKit

    1. David Hyatt, 应用 CSS(第一部分)
    1. David Hyatt, WebCore 概述
    1. David Hyatt, WebCore Rendering
    1. David Hyatt, The FOUC 问题

5. W3C 规范

  1. HTML 4.01 规范
  2. W3C HTML5 规范
  3. 层叠样式表 2 级版本 规范

6. 浏览器构建说明

  1. 火狐 developer.mozilla.org/en/Build_Do…
  2. WebKit webkit.org/building/bu…

转载于:https://juejin.im/post/5c9f1a34f265da30a11b1238

相关文章:

  • 试题 C: 数列求值 蓝桥杯
  • 当遇到css布局,你在考虑什么?
  • PyCharm 2019.1.1 RC 发布
  • 对话Ruby创始人松本行弘、阿里高级技术专家朴灵!
  • 删除Oracle用户及表空间
  • 设计模式:对问题行之有效的解决方式。其实它是一种思想。
  • Linux实战教学笔记02:计算机系统硬件核心知识
  • pytorch Debug —交互式调试工具Pdb (ipdb是增强版的pdb)-1-使用说明
  • 程序结构
  • 设置selinux开机自动关闭
  • Python通用编程 - 第四章:字符编码
  • 微信小程序商店域名由wxapp.dreawer.com永久更换为:store.dreawer.com
  • Java数据解析之JSON
  • 浏览器的兼容性
  • Java基础3:深入理解String及包装类
  • DOM的那些事
  • JS+CSS实现数字滚动
  • js写一个简单的选项卡
  • LeetCode541. Reverse String II -- 按步长反转字符串
  • MySQL QA
  • Python 反序列化安全问题(二)
  • vue从创建到完整的饿了么(11)组件的使用(svg图标及watch的简单使用)
  • 给第三方使用接口的 URL 签名实现
  • 湖南卫视:中国白领因网络偷菜成当代最寂寞的人?
  • 前端_面试
  • 浅谈Kotlin实战篇之自定义View图片圆角简单应用(一)
  • 区块链共识机制优缺点对比都是什么
  • 三栏布局总结
  • 小程序、APP Store 需要的 SSL 证书是个什么东西?
  • 小而合理的前端理论:rscss和rsjs
  • 一个完整Java Web项目背后的密码
  • 追踪解析 FutureTask 源码
  • Nginx实现动静分离
  • 如何用纯 CSS 创作一个菱形 loader 动画
  • ​​​​​​​GitLab 之 GitLab-Runner 安装,配置与问题汇总
  • ​3ds Max插件CG MAGIC图形板块为您提升线条效率!
  • #、%和$符号在OGNL表达式中经常出现
  • #include到底该写在哪
  • %@ page import=%的用法
  • (007)XHTML文档之标题——h1~h6
  • (1)(1.9) MSP (version 4.2)
  • (10)STL算法之搜索(二) 二分查找
  • (cos^2 X)的定积分,求积分 ∫sin^2(x) dx
  • (html转换)StringEscapeUtils类的转义与反转义方法
  • (顺序)容器的好伴侣 --- 容器适配器
  • (四)Android布局类型(线性布局LinearLayout)
  • (原創) 物件導向與老子思想 (OO)
  • (转)h264中avc和flv数据的解析
  • (转)PlayerPrefs在Windows下存到哪里去了?
  • *p++,*(p++),*++p,(*p)++区别?
  • .net core Swagger 过滤部分Api
  • .NET开发人员必知的八个网站
  • .NET性能优化(文摘)
  • .sh文件怎么运行_创建优化的Go镜像文件以及踩过的坑
  • @entity 不限字节长度的类型_一文读懂Redis常见对象类型的底层数据结构