Name: Password: Sign in
浏览器 中的文章
  • 浏览器的工作原理 - 现代浏览器背后的风景
  • 本文在 署名-非商业性使用-相同方式共享 3.0 版权协议下发布, 转载请注明出自 kyleslight.net

    浏览器的工作原理 - 现代浏览器背后的风景

    (本文翻译自 How browsers work 并做适当修改. 翻译过程中的疏漏在所难免,如果你找到了问题希望随时通过邮件 kyleslight@outlook.com 告知 :)

    1. 引论

    浏览器可能是使用最为广泛的软件了. 在本文中我将会解释隐藏在它背后的工作原理. 我们将会看到当你在地址栏中输入「google.com」到你在屏幕上看到 Google 主页时究竟发生了什么.

    1.1 我们将会讨论到的浏览器

    今天我们主要使用五种浏览器 - Internet Explorer,Firefox, Safari, Chrome 与 Opera.

    我将会展示一些开源浏览器的例子 - Firefox,还有部分开源的浏览器 Chrome 与 Safari.

    根据 W3C browser statistics 的统计, 目前(2009年10月)Firefox,Safari 与 Chrome的使用率总和已经将近60%.

    所以说现在开源浏览器是整个浏览器领域的主要部分.

    1.2 浏览器的主要功能

    浏览器的主要功能就是通过向服务器请求你所选择的网络资源将其呈现在浏览器的窗口上. 资源的格式通常是 HTML,当然也有 PDF,图像以及其他. 资源的地址通过用户使用 URI(统一资源标示符)来指定. 在网络这一章我们将更加详细地讨论这个主题.

    浏览器如何解释与呈现 HTML 文件受到 HTML 与 CSS 规范的指定. 这些规范由 W3C(万维网联盟,网络标准的制定组织)维护.

    目前 HTML 的版本是4. 第5版的制定正在进行中,而目前的 CSS 版本号为2,同样它的第3版也正在制定中. 数年来浏览器都只实现了部分规范并独自发展了它们自己的扩展. 这给网站开发者带来了严重的兼容性问题. 今天大多数的浏览器都或多或少地遵从了规范.

    浏览器的用户界面彼此相仿.最普遍的用户界面元素有:

    • 用于输入 URI 的地址栏
    • 前进与返回键
    • 收藏夹选项
    • 用于刷新与停止载入文档流的刷新/停止键
    • 跳转到主页的主页键

    十分奇怪的是,浏览器的用户界面并没有受到任何正式的标准的限定,它只是通过常年来浏览器在互相之间模仿的尝试中形成的不错的结果. HTML5 规范并没有定义一个浏览器必须要有的 UI 元素,只是列举了一些常用的元素,比如地址栏,状态栏以及工具栏.当然,还有一些浏览器独有的特性,像是 Firefox 的下载管理. 在用户界面这一章我们将更详细地讨论这个主题.

    1.3 浏览器的顶层架构

    浏览器的主要模块如下:

    1. 用户界面 - 包括地址栏,前进与返回按钮,收藏夹目录等等,也就是每次请求完页面之后除了主窗口外的所有部分.
    2. 浏览器引擎 - 用来查询与操作渲染引擎的接口.
    3. 渲染引擎 - 负责展示请求的内容.比如如果请求的内容是 HTML 文档的话,渲染引擎就负责解析 HTML 与 CSS 并将解析后的内容显示在屏幕上.
    4. 网络 - 用于网络调用,例如调用 HTTP 请求.它有平台无关的接口以及在每个平台下的实现.
    5. UI 后端 - 用来绘制基本的 Box 组合以及窗口之类的物件. 它向外暴露了一个平台无关的通用接口. 它在底层使用操作系统提供的用户接口方法.
    6. Javascript 解释器 - 用来解析与执行 Javascript 代码.
    7. 数据存储 - 属于持久层.浏览器需要在硬盘上保留所有形式的数据,比如 「cookies」 . 新的 HTML 规范( HTML5 )定义了在浏览器端的完整的(尽管是轻量型的)「web 数据库」.

    Figure 1
    浏览器主要组件

    值得强调的是,Chrome 区别于多数的浏览器,它保持着多个渲染引擎的实例,而且为每个标签页创建一个实例. 任何一个标签页都占据着单独的线程.

    我将为以上每个模块单独拿出一章讲解.

    1.4 模块间的通信

    Firefox 和 Chrome 都发展出了一套特别的通信底层构造.

    我们将特别拿出一章来讨论它们.

    2. 渲染引擎

    渲染引擎的任务就是,额…渲染, 也就是将请求的内容显示在屏幕上.

    默认情况下渲染引擎可以显示 HTML、XML 文档以及图片,同时可以通过插件(浏览器扩展)显示其他类型的内容.比如一个例子就是通过PDF查看插件来显示 PDF 文档. 我们将特别拿出一章来讨论插件与扩展.在这一章中我们将会专注于主要的使用场景 - 借助 CSS 样式来显示 HTML 与图片.

    2.1 渲染引擎导论

    我们参考的浏览器 - Firefox,Chrome 与 Safari 都构建在两种渲染引擎之上. Firefox 使用 Mozilla 自家的渲染引擎 Gecko. Safari 与 Chrome使用的是 Webkit.

    Webkit 是一个起源于 Linux 平台的开源渲染引擎,后来被苹果公司修改,用于支持 Mac 与Windows 平台. 更多细节参考 http://webkit.org/

    2.2 主要流程

    -main-flow

    渲染引擎首先从网络层获取请求文档的内容. 这个过程一般会在8K块中完成.

    在那之后就是渲染引擎的基本流程:


    渲染引擎主要流程

    渲染引擎将会开始解析 HTML 文档并将标签转成「内容树」中的 DOM 节点. 与此同时它将解析外部的 CSS 文档与内部的 CSS 样式元素. 样式信息与 HTML 中可见的标识将会被用来创建另一棵树 - 「渲染树」.

    渲染树包含了带有像颜色,大小等可视属性的矩形块.这些矩形块将会按照正确的顺序显示在屏幕上.

    在渲染树构建完成之后它将会经过一个「布局」过程. 这意味着给定每个节点将要出现在屏幕上的坐标. 接下来一个过程是「绘制」 - 遍历渲染树然后利用 UI 后端层对每个节点进行绘制.

    需要强调的是这是一个循序渐进的过程.为了更好的用户体验,渲染引擎将会尝试尽可能快地将内容呈现在屏幕上. 它不会等到所有的 HTML 被解析完才开始构建与布局渲染树. 当文档剩下的内容还在从网络层接收的时候部分文档就被解析完成并被渲染.

    2.3 主要流程的一些例子

    enter image description here
    Webkit 主要流程

    enter image description here
    Mozilla’s Gecko 渲染引擎主要流程

    从图3和图4中可以看出尽管 Webkit 与 Gecko 使用了略微不同的术语,它们的流程基本是一样的.

    Gecko 将可视的元素组成的树称作「结构树」. 每一个元素是一个结构. Webkit 叫它「渲染树」,它由「渲染对象」组成. Webkit 使用「layout」来摆放元素的位置,而Gecko 叫它「reflow」. 「attanchment」是 Webkit 用来连接 DOM 节点的可视信息创建渲染树的术语. 一个非语义的微小区别在于 Gecko 在 HTML 与 DOM 树间有一个用来创建 DOM 元素的中间层「content sink」. 我们接下来将讨论流程中的每一个部分.

    2.4 解析与 DOM 树的生成

    2.4.1 解析综述

    因为解析是渲染引擎中一个非常重要的过程,所以我们将深入了解它.我们将通过一个简短的介绍来了解解析原理.

    解析一份文档意味着将它翻译成某种有意义并可以被理解与使用的结构.解析的结果通常是一颗用来呈现文档结构并由节点组成的树,我们叫它「解析树」或者「语法树」.

    关于解析表达式「2 + 3 - 1」可能返回的树的一个例子:

    enter image description here
    数学表达式树节点

    2.4.1.1 文法

    解析基于文档中语言或者格式遵循的语法规则.每一种你可以解析的格式必须拥有由令牌与语法规则组成的确定的文法,它被叫做上下文无关的文法.人类的(自然)语言并不遵循这样的文法,因而不能被传统的解析技术解析.

    2.4.1.2 解析器与词法分析器

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

    词法分析是一种将输入的内容分解成令牌的过程. 令牌就是在这门语言中的词汇,也就是所有的有效组成块的集合.对应在人类的语言中就是该语言词典中会出现的所有词.

    语法分析就是这种语言语法规则的运用.

    解析器通常将工作分成两部分 - 分词器负责将输入分解成有效的令牌,解析器负责通过语法规则分析文档结构从而产生解析树. 分词器知道如何将像是空格与换行符这类无关的字符剔除掉.

    enter image description here
    从原文档到解析树

    解析的过程是反复进行的. 解析器通常会向分词器请求新的令牌从而尝试用一条语法规则去匹配这一令牌. 如果一条规则满足,对应于这个令牌的节点就会被添加到解析树上然后解析器请求下一个令牌. 如果没有规则满足,解析器将会在内部保留这个令牌,继续请求下一个令牌直到有一条规则与所有储存起来的令牌相匹配. 如果没有发现匹配的规则,解析器将会报出一个异常. 这意味这份文档是无效的,并且包含语法错误.

    2.4.1.3 翻译

    很多时候解析树并不是最终产物. 解析通常被用来将输入翻译成另一种格式. 一个例子就是编译器. 编译器先是将源代码解析成解析树,然后再将这颗树翻译成机器码.

    enter image description here
    编译流程

    2.4.1.4 关于解析的例子

    在图5中我们从一个数学表达式构建了一个解析树. 我们可以尝试定义一种简单的数学语言来看看解析的过程.

    词汇:
    该语言包含有整数,加号与减号

    语法:

    1. 该语言语法的构建块是表达式,项与操作符号
    2. 该语言可以包含任意数量的表达式
    3. 一个表达式被定义成一个「项」接着一个「操作符号」再接着一个「项」
    4. 一个操作符号是一个加法令牌或者一个减法令牌
    5. 一个项是一个整数或者一个表达式

    让我们来分析一下输入「2 + 3 - 1」.

    第一个满足一条语法的子串是“2”,根据第5条规则它是一个「项」. 第二个满足的是「2 + 3」,它满足第三条规则 - 一个「项」跟着一个「操作符号」再跟着另一个「项」. 接下来只有到达输入末端的时候满足要求. 因为我们已经知道「2 + 3」是一个「项」所以我们又得到了一个「项」跟着一个「操作符号」再跟着另一个项,「2++」不会满足任何规则,所以是一个非法的输入.

    2.4.1.5 词汇与语法的正式定义

    词汇通常用「正则表达式」来表示.

    对于我们的例子语言可以定义如下:

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

    就像你所看到的那样,(正)整数是通过正则表达式来定义的.
    语法通常用一种叫做「BNF」的格式来定义.在我们的语言中可以定义如下:

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

    如果一种语言的语法满足上下文无关文法,那么我们说该语言可以被正则解析器解析. 一个直观的上下文无关文法的定义就是这种语法可以完全被BNF来表达.更为正式的定义参考 http://en.wikipedia.org/wiki/Context-free_grammar

    2.4.1.6 解析器的种类

    有两种基本的解析器-自顶向下的解析器与自底向上的解析器. 一个直观的解释就是自顶向下的解析器从语法结构的顶层出发尝试匹配任何一个语法规则,自底向上的解析器开始于输入然后逐渐将其转化成语法规则,从底层规则出发直到高层的语法规则满足.

    让我们来看看两种解析器如何来解析上面的例子:

    自顶向下的解析器从顶层规则出发 - 它将会把「2 + 3」标志成一个表达式. 然后它将「2 + 3 - 1」标志成一个表达式(标志的过程会关联到其他的规则,但是我们从最顶层的规则出发).

    自底向上的解析器将会扫描输入直到满足一条规则,然后用这条规则替换掉满足条件的输入. 这个过程将会持续进行直到输入的末端.部分满足的表达式将会被放入解析器栈.

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

    这种自底向上的解释器又称作移位削减解释器,因为输入向右移动(想象有一个指针指向输入的第一位然后移向右端)而且根据语法规则逐渐减少.

    2.4.1.7 自动生成解释器

    有很多工具可以为你产生解释器. 他们被称作解释器生成器. 给它输入你的语言的文法(包括词汇与语法规则)然后它们将生成一个可以工作的解释器. 独立创造一个解释器需要深入理解解析原理,而且手写一个经过优化的解释器并不容易,所以说解释器生成器非常有用.

    Webkit 使用两种著名的解释器生成器 - 创建分词器的 Flex 以及创建解释器的 Bison(你可能偶然听说过 Lex 与 Yacc ). Flex 的输入是一个包含定义令牌的正则表达式文件.Bison 的输入BNF格式的该语言的语法规则.

    2.4.2 HTML 解释器

    HTML 解释器的工作就是将 HTML 标签解释成解释树.

    2.4.2.1 HTML 文法定义

    HTML 的词汇与语法由 W3C 的规范定义. 目前的版本为 HTML4,HTML5 的标准正在制定中.

    2.4.2.2 HTML 不是上下文无关的文法

    就像我们在解析导论中看到的,文法语法可以使用类似于 BNF 的格式来定义.

    不幸的是所有传统的解析器一般规则都不能运用在 HTML 上(当然我之前提它们不是用来开玩笑的 - 他们可以用来解析 CSS 与 Javascript ). HTML 不能用解析器需要的上下文无关文法来简单定义.

    还是有一种正式的格式来定义 HTML - DTD(文档类型定义)- 不过它不是上下文无关文法.

    初看上去似乎很奇怪 - HTML 与 XML 很像. 有很多可用的 XML 解析器.甚至还有 XML 对应于 HTML 的变种 - XHTML - 所以说为什么会有如此巨大的差异呢?

    差异在于 HTML 非常“宽容”,它允许你漏掉一些原本应该隐式加上的标签,有时候你可以漏掉标签头或尾等等. 总的来说它是一种宽松的语法,与 XML 严格且吃力的语法相区别.

    显然很小的差异将会导致巨大的变化. 一方面这是 HTML 如此流行的原因 - 它会宽容你的错误,同时让网站构建者的生活变得容易. 另一方面,它使得写出格式化的文法变得困难. 所以总的来说,HTML 不能被简单地解析,因为它不是上下文无关文法所以不能被传统的解析器解析,也不能被 XML 解析器解析.

    2.4.2.3 HTML DTD

    HTML 使用 DTD 格式来定义.这种格式用来定义 SGML 家族的语言. 它包含了所有允许的元素,属性以及层级关系的定义. 正如我们之前看到的,HTML DTD并不形成一种上下文无关的文法.

    DTD 有很多变种.比较严格的类型完全遵循标准. 但是一些其他的类型可以支持以前浏览器使用的标记. 目的就是为了向前兼容早期的内容. 目前所用的严格 DTD 可以参照 http://www.w3.org/TR/html4/strict.dtd

    2.4.2.4 DOM

    HTML 解析器产生的结果 - 「解析树」 是一种由 DOM 元素与属性节点组成的树. DOM 是文档对象模型(Document Object Model)的简称. 它是 HTML 文档的对象呈现,同时也是HTML 元素向外界(例如 Javascript )暴露的接口.

    DOM 树的根节点是 Document 对象.

    DOM 与 HTML 标记几乎一一对应.举个例子,比如这个标记:

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

    将会被翻译成如下的DOM树:

    enter image description here
    样例标记的 DOM 树

    与 HTML 一样,DOM 的标准也由 W3C 制定. 参见 http://www.w3.org/DOM/DOMTR.它是操作文档的通用规范. 一个特定的模块对应着 HTML 中特定的元素.关于 HTML 的定义可以参考 http://www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html

    当我说一棵树包含着 DOM 节点的时候,我其实是指这棵树由执行某一 DOM 接口的元素组成.浏览器使用的特定实现有着浏览器自身提供的其他属性.

    2.4.2.5 解析算法

    如我们在之前的部分中看到的,HTML 不能被常规的自顶向下的解析器或自底向上的解析器解析.

    原因如下:

    1. 该语言的宽松性.
    2. 浏览器有对支持著名的非法 HTML 错误容忍的传统.
    3. 解析的过程是可重入的.通常源代码在解析的过程中并不会改变,但是在 HTML 中, script 标签包含着 document.write 语句可以增加新的令牌,所以解析过程事实上修改了输入.

    由于无法使用常规的解析技术,浏览器创建了特定的解析器来解析 HTML.

    解析算法由 HTML5 规范详细描述.这种算法包含两个阶段 - 分词与生成树.

    分词就是词法分析,将输入解析成令牌. HTML 中的令牌包括起始标签,终止标签,属性名与属性值.

    分词器可以识别令牌,然后将其交给树构造器然后接收下一个字符来识别下一个令牌,循环往复直到输入的终止.

    enter image description here
    HTML 解析流程( 摘自 HTML5 规范 )

    2.4.2.6 分词算法

    算法的输出是 HTML 令牌. 这种算法由状态机来描述. 每一个状态接收一个或多个输入流中的字符,然后根据这些字符来更新下一个状态. 状态机的决策受到当前分词状态与生成树状态的共同影响. 这意味着接收同样的字符将会根据当前的状态产生不同的结果,这些结果会对下一个状态产生影响. 这个算法要完全写出来非常复杂,所以让我们通过一个简单的例子来理解其中的原理.

    基本的例子 - 对以下 HTML 进行分词:

    <html>
        <body>
            Hello World
        </body>
    </html>
    

    初始的状态是「数据态」. 当我们遇到<符号时,状态将变为「开标签态」. 接收一个a-z字符将会导致「开始标签令牌」创建,然后状态将改为「标签名态」. 我们将会保持这种状态直到接收到一个>字符. 每一个字符将会增加到新的令牌名中. 在我们的例子中创建的是一个html令牌.

    当到达>标签时,当前的令牌将会释放,状态重新返回至“数据态”. <body> 标签也会由同样的步骤处理.到目前为止 htmlbody 标签被释放了. 我们又重新回到了「数据态」. 接收 Hello World 中的 H 字符将会创建并释放一个字符令牌,这个过程将会持续直到我们遇到 </body> 中的 < . 我们将会释放 Hello World 中的每一个字符令牌.然后我们回到了「开标签态」. 接收到下一个输入 / 将会创建一个「终止标签令牌」然后移动到「标签名态」. 我们又一次处于这种状态直到我们遇到 > . 然后新的标签令牌将会释放我们重新回到“数据态”. </html> 的处理方式与之前相同.

    enter image description here
    样例输入的分词过程

    2.4.2.7 构造树算法

    当解析器被创建的时候文档对象就被创建了. 在树的创建阶段以 Document 为根节点的 DOM树将会被修改然后元素将会被添加上去. 每一个由分词器释放的节点将会被传送到树的生成器上. 对于每一个令牌规范都定义了哪些相关的 DOM 元素将会随之被创建.除了在 DOM 树中添加节点它将会将打开的元素压入栈中. 这种栈被用来正确存储未被匹配的或未关闭的标签. 这种算法同样可以被描述为一个状态机. 这种状态被称为「插入态」.

    让我们来看看对于样例输入树的构造过程.

    <html>
        <body>
            Hello World
        </body>
    </html>
    

    树的构造阶段的输入是一系列由分词阶段产生的令牌.第一种模式是「起始」模式.接收到 HTML 令牌后将会转移到「在HTML之前」模式然后在那种模式下重新处理令牌. 这将会导致 HTMLHtmlElement 元素被创建然后被添加到根文档对象后面. 然后状态变为「在head之前」. 我们接收到了 body 令牌. 一个 HTMLHeadElement 元素 将会被隐式地创建,尽管我们并没有 head 令牌它仍将被添加到树上.

    我们现在转移到「在head中」模式然后是「在head后」. 这时候 body 令牌又被重新处理了,HTMLBodyElement 元素被创建然后被插入,模式又转变为「在body中」.

    我们又接收到了由「Hello World」字符串组成的字符令牌. 第一个字符令牌将会让一个 Text 节点被创建与插入,其他的字符也会被添加到那个节点下. 接收到 body 终止令牌将会导致状态转变到 「在body后」 . 然后我们现在接收到了 html 终止标签,这将会是我们转移到 「在body之后的之后」模式. 接收到文件结束令牌将会终止解析过程.

    enter image description here
    样例 HTML 树的构造

    2.4.2.8 解析完成之后的动作

    在这个阶段浏览器将会把文档标记成交互状态然后开始在「deferred」模式(在文档被解析完成后需要被执行的模式)解析脚本. 文档状态之后将会被设置为「完成」然后触发「load」事件.

    你可以在这里看到完整的HTML规范下的分词与生成树算法 - http://www.w3.org/TR/html5/syntax.html#html-parser

    2.4.2.9 浏览器的容错

    在 HTML 页面上你将永远不会遇到「无效语法」这样的错误.浏览器会修复无效的内容然后继续执行.

    用下面这串 HTML 作为例子:

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

    我一定违反了上百万条规则( mytag 并不是标准的标签,错误的 pdiv 元素嵌套以及其他 )但是浏览器依然能够毫不抱怨地正确显示它. 所以可见有大量的解析器代码用来修正 HTML 作者的错误.

    不同浏览器间对错误的处理十分一致,但令人惊奇的是这并不是当前 HTML 标准的一部分. 就像书签与返回/前进按钮这种不成文的规则一样,它只是浏览器发展多年形成的产物. 一些有名的非法 HTML 结构频频出现在大量站点中,浏览器不得不与其他浏览器一道用一致的方式修正它们.

    HTML5 规范确实定义了一些要求. Webkit 在它的 HTML 解析器类开头的注释中作出了很好的总结:

    The parser parses tokenized input into the document, building up the document tree. If the document is well-formed, parsing it is straightforward.

    Unfortunately, we have to handle many HTML documents that are not well-formed, so the parser has to be tolerant about errors.

    We have to take care of at least the following error conditions:

    1. The element being added is explicitly forbidden inside some outer tag.
      In this case we should close all tags up to the one, which forbids the element, and add it afterwards.

    2. We are not allowed to add the element directly.
      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.

    让我们看看 Webkit 容错的一些例子:

    </br> instead if <br>
    

    有一些站点会使用 </br> 而不是 <br> . 为了兼容 IE 与 Firefox,Webkit 将它们都当作 <br> 处理.

    以下是代码:

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

    值得注意的是,错误处理是浏览器内部的,它不会呈现给用户.

    2.4.2.9.1 零散的表格

    零散的表格是指一个表格被嵌入到另一个表格的内容里但并不是在「table cell」中.

    就像这个例子:

    <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 &amp;&amp; localName == tableTag)
            popBlock(tableTag);
    

    Webkit 使用栈来存储当前元素的内容,它将会将里面的表格从外面的表格元素中弹出.现在这两个表格就是兄弟元素了.

    2.4.2.9.2 嵌套表单元素

    如果用户将一个表单放入另一个表单,第二个表单将被忽略.

    以下是代码:

    if (!m_currentFormElement) {
            m_currentFormElement = new HTMLFormElement(formTag,m_document);
    }
    
    2.4.2.9.3 过深的标签层级

    注释里是这样说的:

    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&amp; tagName)
    {
    
    unsigned i = 0;
    for (HTMLStackElem* curr = m_blockStack;
             i < cMaxRedundantTagDepth &amp;&amp; curr &amp;&amp; curr->tagName == tagName;
         curr = curr->next, i++) { }
    return i != cMaxRedundantTagDepth;
    }
    
    2.4.2.9.4 放错位置的html或者body标签

    我们再来看看注释怎么说:

    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;
    

    所以说 web 作者们要小心了,除非你想让你的代码出现在 Webkit 容错代码的例子中,否则请写结构良好的 HTML.

    2.4.3 CSS 解析

    记得导论中的概念吗?很好,不像HTML,CSS 遵循上下文无关文法,并且可以用各种导论中描述的解析器解析. 事实上 CSS 规范定义了 CSS 的分词与语法文法(http://www.w3.org/TR/CSS2/grammar.html).

    让我们看看一些例子:

    分词文法(词汇)的每个令牌都由正则表达式定义:

    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」是标识器的简称,例如 class 名. 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 表示 空格)隔开的选择器. 规则集合包含有花括号以及其内部的一个或多个由分号隔开的属性声明. 「属性声明」与「选择器」将由下面的 BNF 格式定义.

    2.4.3.1 CSS 解析器

    Webkit 使用 Flex 与 Bison 解析器生成器从 CSS 文法文件自动创造解析器. 回忆一下解析器导论中的内容,Bison 创建自底向上减缩的解析器. Firefox 使用自己手写的自顶向下的解析器.这两种情况都将 CSS 解析成一个样式表对象,每一个对象都包含有 CSS 规则. CSS 规则对象包含有选择器以及属性声明对象以及其他 CSS 文法相应的对象.

    enter image description here
    解析 CSS

    2.4.4 解析脚本

    这章节将会来处理Javascript脚本.

    2.4.4.1 处理脚本与样式表的先后顺序
    2.4.4.1.1 脚本

    网络中的模型都是同步的. 代码的作者们期待只要解析器遇到一个 <script> 标签就会立即解析并执行里面的脚本. 事实上解析文档的过程到等到脚本执行之后.如果脚本是外部的,那么资源一定先从网络层获取,而且这个过程也是同步的,只有当所有资源获取完毕之后才会开始解析. 这是多年以来使用的模型,同样也是 HTML4 与 HTML5 规范指定的. 代码作者可以将脚本标记为“延迟”那么它将不会等到文档解析完成而是会解析完成之后立即执行. HTML5 增加了标记的选项使得文档可以使用另一个线程中被解析与执行.

    2.4.4.1.2 推理型解析

    Webkit 与 Firefox 都会做这个优化.当执行脚本的时候,另一个线程将会解析剩下的文档然后找到需要从网络加载的资源并加载它们.通过这种方式资源可以通过平行的连接加载而总的来说速度会更快. 值得注意的是 - 推测型解析并没有改变 DOM 树,而是将它留给主解析器解析,它只解析外部资源的引用,像是外部脚本,样式表以及图片.

    2.4.4.1.3 样式表

    样式表使用不同的模型.概念上说看起来因为样式表并不会改变 DOM 树,所以也没有理由等待样式表解析而停止文档解析. 但是还是有一个问题,在文档解析阶段脚本可能会要求样式信息. 如果样式表没被加载与解析,脚本将会得到错误的信息,而且很明显这会导致大量的问题. 这看起来是个边缘的问题,其实非常常见.只要还有一个样式没被加载与解析,Firefox就会阻止掉所有的脚本执行. Webkit 更聪明一点,只有当脚本尝试去获取特定的样式属性的,而那些属性可能会被没加载的样式表影响的时候才会阻止执行这些脚本.

    2.5 构造渲染树

    当 DOM 树被构造完成后,浏览器将会构造另一棵树 - 渲染树. 这棵树是由可见元素按照它们将会被显示的顺序排成. 它是文档文档的可视化呈现.这棵树的作用是使得按照正确顺序绘制内容成为可能.

    Firefox 将渲染树中的元素称为「frame」. 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 根据 display 属性决定为一个 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;
    }
    

    同样需要考虑元素的类型,比如表单控制以及表格都有特殊的渲染器. 在 Webkit 中如果一个元素想要创建一个特殊的渲染器它将会重载掉 createRenderer 方法. 渲染器指向包含着非几何信息的样式对象.

    2.5.1 渲染树于DOM树的关系

    渲染器对应着 DOM 中的元素,但关系不是一一对应.不可视的 DOM 元素将不会插入到渲染树中. 一个典型的例子就是 head 元素. 另外一些 display 属性值为 none 的元素也不会出现在渲染树中(但是有 hidden 可视属性的元素会出现在渲染树中).

    有一些 DOM 元素对应多个可视对象. 通常有一些元素有着复杂的结构不能用单个矩形来描述.比如,select 元素有三个渲染器,一个对应着显示区域,一个对应下拉的列表框,还有一个对应按钮. 同样当文本由于单行的宽度不够而折成多行时,新的行将会被添加到另外的渲染器中.

    另一个多渲染器的例子是缺损的 HTML. 根据 CSS 规范一个行内元素必须包含要么只有块级元素,要么只有行内元素. 当混合元素出现时,匿名的块渲染器就会被创建用来包含行内元素.

    有些渲染对象虽然对应DOM节点但是并不在树的同一位置. 浮动和绝对定位的元素都会脱离文档流,被放置在树的另一个地方,而且映射一个真实的 「frame」 . 它们应该被看到的地方就是一个占位用的框架.

    enter image description here
    渲染树与对应的 DOM 树. 「Viewport」是初始的容器块.在 Webkit 中对应的将是 RenderView 对象.

    2.5.2 构造树的流程

    在 Firefox 中,presentation 被注册为一个用来检测 DOM 更新的监听器. presentation 将渲染器的创建代理给「FrameConstructor」然后这个来解决样式(参见样式计算)并创建一个渲染器.

    在 Webkit 中解决样式并创建渲染器的过程被称为「attachment」. 每一个 DOM 节点都有一个 「attach」方法. attachment 是同步的,节点插入到 DOM 树就会调用新的节点 「attach」方法.

    处理 htmlbody 标签将会导致渲染树根节点的构造根渲染对象对应着 CSS 规范中的「containing block」 - 一个包含着所有其他块的顶层块. 它的尺寸是 「viewport」,也就是浏览器窗口显示区域的尺寸. Firefox 叫它 「ViewPortFrame」,Webkit 叫它 「RenderView」. 这是文档指向的渲染对象. 树的其他部分由 DOM 节点的插入来构造. 参见 CSS2 中的该主题 - http://www.w3.org/TR/CSS21/intro.html#processing-model

    2.5.3 样式的计算

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

    样式包含有来自不同源的样式表,行内 style 元素以及 HTML 中可视的属性(比如 bgcolor 属性). 后者被翻译后将会匹配 CSS 样式属性.

    样式表的来源有浏览器默认样式表,页面作者提供的样式表以及用户样式表 - 由浏览器用户提供的样式表(浏览器允许你定义你自己的样式.举个例子,在 Firefox 中,在 「Firefox Profile」文件夹中放入一个样式表就可以完成定义自己的样式).

    样式计算带来了一些困难:

    1. 样式的数据时非常大的结构,包含有大量的属性,这可能会导致内存问题.

    2. 如果没有优化的话,为每个元素找到匹配的规则可能会导致性能问题. 为每个元素找到对应的规则而去遍历整个规则表将是一笔巨大的开销. 选择器可能有复杂的结构,可能会导致匹配过程从一个看起来可信的路径到被证明是错误的,然后不得不尝试另一条路径.比如这个复合选择器:

    div div div div{
    ...
    }
    

    意味着规则应用的 <div> 是三个 <div> 的后代. 试想你想要检查这条规则是否可以运用于给定的 <div> 元素. 你选择从一条特定的树出发. 你可能需要遍历遍历节点树结果只发现两层div所以规则无效. 然后你将尝试另一条路径.

    1. 应用规则可能会牵扯到十分复杂的层叠规则.

    让我们来看看浏览器如何来解决这些问题:

    2.5.3.1 分享样式数据

    Webkit 节点引用样式对象 「RenderStyle」. 这些对象可以在某些条件下被分享. 这些节点是兄弟节点或者表兄弟节点与:

    1. 这些元素必须是处于同样的鼠标状态(例如,不能一个处于 :hover 而另一个不是)
    2. 两个元素不能有同一个 id
    3. 它们的标签名相同
    4. class 属性需要相同
    5. 映射后的属性集合必须相同
    6. 链接状态必须相同
    7. 获取焦点状态必须相同
    8. 任何一个元素都不能被属性选择器影响. 所谓的影响是指有任何选择器在选择器的任何地方使用属性选择器
    9. 这些元素不能有行内样式标签
    10. 不能有兄弟选择器正在被使用. 当有兄弟选择器正在被使用时而选择器正在被呈现时,WebCore 将会简单地抛出一个全局开关然后禁掉整个文档的样式分享. 兄弟选择器包括“+”选择器以及其他像是 :first-child:last-child 的选择器.
    2.5.3.2 Firefox 规则树

    Firefox 有两个用来简便计算的额外的树 - 规则树与样式上下文树. Webkit 也有样式对象,不过它们不存储在像是样式上下文树之类的树中,只有 DOM 节点指向它们引用的样式.

    enter image description here
    Firefox 样式上下文树

    样式上下文包含有计算完成后的最终结果. 这些结果通过按照正确的顺序应用所有匹配的规则以及执行将它们从逻辑转成具体值计算而来. 比如,如果逻辑值是占屏幕的百分比,它将会被计算然后转成绝对单位. 规则树的想法非常聪明. 它使得节点间分值从而避免重复计算变得可行.它同样也省空间.

    所有被匹配的规则都将被存储到一棵树中. 底部的节点拥有更高的权重. 这棵树包含有所有规则匹配到的路径. 存储规则是惰性的. 一开始树中的每个节点都不会被计算,但是一旦一个节点样式需要被计算计算后的路径将会被添加到树上.

    想法就是将树的路径看做词典中的单词. 比如说我们已经计算好了这颗规则树:

    enter image description here

    假如我们需要为内容树的另一个节点匹配规则,然后发现匹配的规则(在正确的顺序下)是「B - E - I」. 因为我们已经计算了路径 「A - B - E - I - L」,所以我们已经在树中有了这条路径.我们现在就有更少的工作要做了.

    让我们看看树中存储的内容如何工作.

    2.5.3.2.1 分解成结构

    样式上下文将被分解成结构.这些结构包含了某些特定类别的样式信息,比如边框或者颜色.所有结构中的属性要么是继承而来要么是非继承而来.继承的属性除非由元素定义,否则由父类继承而来. 没有继承的属性(叫做「重置」属性)如果没有定义的话使用默认值.

    这棵树帮我们缓存了整个树中的结构(包括计算的最终值). 这样的想法就是如果底部的节点不提供结构的定义,那么上层的缓存结构就可以被使用.

    2.5.3.2.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>
    

    并且规则如下:

    1.    div {margin:5px;color:black}
    2.    .err {color:red}
    3.    .big {margin-top:3px}
    4.    div span {margin-bottom:4px}
    5.    #div1 {color:blue}
    6.    #div 2 {color:green}
    

    为了简单起见比如说我们需要只填充两个结构:color 结构与 margin 结构. color 结构只包含一个成员 color. margin 结构包含四条边的成员信息.

    结果规则树将会长成这个样子(节点由节点名标记:也就是它们指向的 # 规则)

    enter image description here
    规则树

    上下文树将会长成这个样子(节点名:它们指向的规则)

    enter image description here
    上下文树

    假设我们解析 HTML 然后得到了第二个 <div> 标签.我们需要为这个节点创造一个新的样式上下文然后填充它的样式结构.

    我们将会匹配规则发现匹配第二个 <div> 的规则是1,2与6. 这意味着在树中已经存在我们可以使用的一条路径,我们只需要为规则6添加另一个节点(规则树中的F节点)即可.

    我们将会创建一个样式上下文将它放入上下文树中. 新的样式上下文将会指向规则树中的 F 节点.

    我们现在需要填充样式结构.我们将从填充 margin 结构开始. 因为最后一条规则节点(F)并没有添加到 margin 结构上,我们可以向上遍历树,直到我们发现了一个之前插入节点时计算过的缓存结构就可以使用这个结构了.我们将会这个结构在节点 B 上,这个是指定 margin 规则的最顶层的节点.

    我们已经有了 color 结构的定义,所以我们不能使用缓存的结构.因为颜色只有一个属性我们并不需要沿着树向上去填充其他的属性.我们将会计算最终值(将字符串转化为 RGB 等等)然后将计算后的结构缓存至这个节点上.

    对第二个 <span>元素的处理工作同样简单. 我们将会匹配规则然后得到结论这个元素指向规则G,就像上一个span一样. 因为我们有兄弟元素指向同一个节点,所以我们可以分享整个样式上下文,将此元素指向之前 span 元素的上下文.

    其他结构包含从上一级继承的规则,此时上下文树缓存完成(颜色属性事实上是继承的,不过Firefox将它作默认处理,并将它缓存到规则树上).

    比如如果我们想在一个 paragraph 中添加 font 规则:

    p {font-family:Verdana;font size:10px;font-weight:bold}
    

    然后div元素,也就是上下文树 paragraph 的后代,可以从父节点分享到同样的 font 结构. 这就是如果没有给 div 添加 font 规则的默认指定方式.

    在 Webkit 中并没有规则树,匹配的定义将会被遍历四次. 第一次是非 important 的高权重属性(一些由于其他属性需要依靠它们所以一开始就应该被应用的属性 - 像是 display )的应用,然后是标记了 important 的高权重属性,然后是普通非 important 属性,最后是标有 important 的普通属性. 这意味着属性多次出现的属性将会根据正确的层叠顺序重新处理.最后出现的将覆盖之前的.

    所以总结来说,分享样式对象(整个或者部分)解决了问题 1和 3. Firefox 规则树同样帮助我们用正确的顺序来运用这些属性.

    2.5.3.3 为一个简单的匹配操作规则

    有这样几种样式规则的来源:

    • CSS 规则,无论是外部的样式表或者在元素中定义的样式表.
    p {color:blue}
    
    • 行内样式属性
    <p style="color:blue" />
    
    • HTML可视化属性(这些将会被映射到相关样式规则中)
    <p bgcolor="blue" />
    

    最后的两个可以很容易匹配到元素,因为它们包含了这些样式属性所以 HTML 属性可以将元素作为键来映射.

    像之前的问题2中强调的,这些 CSS 规则匹配起来非常棘手. 为了解决困难,规则使用更简单的方式来操作.

    在解析样式表之后,这些规则根据选择器被添加到哈希映射. 它们通过 id名,class名,tag 名还有一个用来为任何不属于这些分类的通用的映射. 如果一个选择器是一个id ,规则将会被添加到 id 映射中,如果是一个 class 就添加到 class 映射中等等.

    这种操作使得匹配规则变得更加容易.再没有必要去查找每个声明 - 我们可以为一个元素从映射中提取相关规则. 这种优化减少了95%以上的规则匹配过程,所以它们在匹配过程中不再需要被考虑.

    举个例子,让我们看看下面这些样式规则:

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

    第一条规则将会被插入到 class 映射中.第二条是 id 映射,第三条是 tag 映射.

    用于下面的 HTML 片段;

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

    我们将会尝试先为 p 元素寻找规则. 在 class 映射下将会包含 error 的键,在这种条件下 p.error 规则被发现. div 元素将会有在 id 映射(键就是 id )中与 tag 映射中的相关规则. 所以唯一的工作就是通过键找到它们真正匹配的提取出来的规则.

    比如如果对于 div 有以下规则

    table div {margin:5px}
    

    它将同样从 tag 映射中提取,因为键值是最右边的选择器,但是它将不会匹配到我们的div元素,因为它并没有一个 table 父辈.

    Webkit 与 Firefox 都会执行这种操作.

    2.5.3.4 在正确的层叠顺序下运用规则

    样式对象对每个可视属性都有相应的属性(所有的 CSS 属性,不过更多的是通用的属性). 如果属性并没有被任何匹配的规则定义 - 那么一些属性将会由父元素样式对象继承.另一些属性有默认值.

    问题开始出现在多于一个定义上 - 所以出现了层叠顺序用来解决这个问题.

    2.5.3.4.1 样式表层叠顺序

    样式属性的声明可以出现在多个样式表中,也可以在一份样式表中出现多次. 这意味着应用规则的顺序非常重要. 这被叫做「层叠」顺序. 根据 CSS2 规范,层叠顺序是指(从低到高):

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

    浏览器声明重要程度最低,用户唯一能够覆盖网页作者的方式就是标记 important . 在同一顺序声明将会先根据特殊性,再根据它们被指定的顺序被存放起来. HTML 可视属性将被翻译用来匹配声明. 它们将被处理成网页作者低权重规则(普通声明).

    2.5.3.4.2 特殊性

    选择器的由 CSS2 规范定义如下:

    • 如果是从「样式」属性而不是选择器规则中声明,记为1,否则记0(=a)
    • 选择器中ID属性的序数号(=b)
    • 选择器中其他属性以及伪类的序数(=c)
    • 选择器中元素名以及伪元素的序数(=d)

    连接四个数 「a-b-c-d」 (在数字系统中相当于有一个很大的进制)即为特殊性.

    你需要使用的数字进制由每个分类中可能出现的最大的数字决定.

    比如,如果 a=14 ,你可以使用十六进制.在一个不大可能的情况,比如 a=17 中你将会需要将 17 作为基.后者可能会出现在选择器像这样的情况:html body div div p...(选择器中有17个 tag …几乎不大可能出现).

    例如一些例子:

    *             {}  /* 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 */
    
    2.5.3.4.3 为规则排序

    规则被匹配之后,它们将根据层叠规则储存.Webkit 对小型列表使用冒泡排序,对大型列表使用并归排序.Webkit 使用重载 > 运算符来实现规则排序:

    static bool operator >(CSSRuleData&amp; r1, CSSRuleData&amp; r2)
    {
        int spec1 = r1.selector()->specificity();
        int spec2 = r2.selector()->specificity();
        return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2; 
    }
    
    2.5.3.5 循序渐进的过程

    Webkit 使用 flag 来标记是否所有的顶层样式表(包括 @imports )已经被载入. 如果在附着过程中样式没有完全载入将会使用占位符并在文档中做标记,它们将会在样式表被载入完成后被重新计算.

    2.6 布局

    当渲染器被创建并被添加到树上后,它并没有并没有位置与大小的信息.

    计算这些值的过程叫做布局(layout)或回流(reflow).

    HTML 使用基于流的布局模型,这意味着大多数时候在单通道上计算几何信息是可行的. “在流中”相对后的元素通常不会影响「在流中」的之前的元素的几何信息,所以布局过程可以沿着整个文档处理由左至右,由上至下的内容.但是有一些例外, 比如,HTML table 可能会需要超过一条通道.

    坐标系统是相对于「根frame」的,同时使用左上角坐标系.

    布局是一个递归的过程.它从根节点出发(对应于 HTML 的 document 元素). 布局过程从一些或者所有 frame 结构递归地持续下去,计算每一个渲染器请求的几何信息.

    根渲染器的位置是 (0, 0),它的大小是「viewport」,也就是浏览器窗口的可视部分.

    所有的渲染器都有「布局」或者「回流」方法,每一个渲染器调用需要布局的子渲染器的布局方法.

    2.6.1 页面重写标识(Dirty Bit)系统

    为了对每个小的改变不做整体的布局过程,浏览器使用「页面重写标识」系统.一个改变的渲染器会给自己或自己后代中添加了「重写」号的渲染器重新布局.

    有两种记号 - 「重写」于「后代需要重写」. 后代重写意味着尽管渲染器本身没有问题,但是至少它的一个后代需要重新布局.

    2.6.2 全局布局与增量式布局

    布局过程可以在整个渲染树上被触发 - 这被称作「全局」布局. 它将由以下原因导致:

    1. 一个影响所有渲染器的全局样式变化,比如字体大小变化.
    2. 屏幕大小被调整.

    布局可以使增量式的,只有标有重写的渲染器才会被重新布局(这可能会导致一些依赖外部布局的渲染器的损害).

    当渲染器被标记重写时增量式布局将会被触发(异步进行). 比如当获得从网络层来的外部内容并被添加到 DOM 树后新的渲染器就会被添加到渲染树上.

    enter image description here
    增量式布局 - 只有需要重写的渲染器与它们的后代需要重新布局

    2.6.3 异步与同步布局

    增量式布局是异步完成的. Firefox 给增量式布局「回流命令」排队,然后一个调度器将会触发这些命令的批处理执行. Webkit 同样有一个计时器用来执行一次增量式布局 - 遍历渲染树,然后需要重写的渲染器就被重新布局.

    脚本将会请求样式信息,像是 offsightHeight 可以同步触发增量式布局.

    全局布局通常是同步触发的.

    有时候布局将会作为初始布局一些属性(例如滚动位置)的变化之后的回调函数而被触发.

    2.6.4 优化

    当一次布局被「调整大小」或者渲染器位置(不是大小)变化而触发时,渲染器大小将从缓存中取出而不需要重新计算.

    在一些情况下 - 只有子树被修改那么布局过程就不会从根节点出发. 这种情况可能发生在局部变化但是并不影响周围内容 - 像是文本被插入到文本域里(否则每次按键就会触发一次从根节点开始的布局过程).

    2.6.5 布局过程

    布局通常有以下模式:

    1. 父渲染器决定了自己的宽度.
    2. 父渲染器检查后代然后:
      1. 放置子渲染器(设定它的x与y值).
      2. 如果需要就调用后代的布局过程(比如它们被标记了需要重写或者处于全局布局阶段或者一些其他的原因)- 这个计算后代的高度.
    3. 父渲染器使用后代累计高度与内外边框的高度来设定自己的高度 - 而这也将被用于父渲染器的父渲染器.
    4. 将重写标签设为false.

    Firefox使用一个「状态」对象( nsHTMLReflowState )作为布局的参数(被称为「reflow」). 在其他的浏览器中状态包含了父渲染器的宽度.

    Firefox 布局之后的结果是一个「度量(metrics)」对象( nsHTMLReflowMetrics ).它将包含渲染器计算后的高度.

    2.6.6 宽度计算

    渲染器的宽度使用容器块的宽度,渲染器的样式属性 widthmarginborder 进行计算.

    比如下面这个 div 块的宽度:

    <div style="width:30%"/>
    

    将会由 Webkit 使用以下方式计算( RenderBox 类的方法 calcWidth ):

    • 容器的宽度是容器可能宽度与0的较大值. 在这个例子中可能的宽度是由以下方式计算得到的内容宽度:
    clientWidth() - paddingLeft() - paddingRight()
    

    clientWidthclientHeight 度量了一个对象排除边框与滚动条之后的内部区域.

    • 元素的宽度由 width 样式属性决定. 它将通过计算容器宽度的百分比来得到一个绝对值.
    • 水平方向的边框与内边距被添加上去.

    目前为止计算的是「首选的宽度」. 现在宽度的最小量与最大量将被计算.

    如果首选宽度大于最大宽度,那么最大宽度将被使用.如果小于最小宽度(最小的不可换行的单元)那么最小宽度将被使用.

    这个值将被缓存,用于当布局时需要但是宽度不变的场合.

    2.6.7 自动换行

    一个渲染器在布局的过程中决定它是否需要换行. 它将停止工作并告诉父节点需要换行. 父节点将会创建一个额外的渲染器然后调用上面的布局方法.

    2.7 绘制

    在绘制阶段,渲染树将会被遍历然后渲染器的「paint」方法将会被调用来在屏幕上显示内容. 绘制使用 UI 基础组件. 更多内容将在 UI 这章讨论.

    2.7.1 全局式与增量式

    就像布局一样,绘制同样可以是全局 - 对整棵树进行绘制 - 或者增量式. 在增量式绘制中,一些渲染器在不影响整棵树的条件下变化. 这些改变了的渲染器使得它在屏幕上显示的矩形区域无效化. 这将导致操作系统将其视为「需要重写的区域」然后产生一个「paint」事件. 操作系统会非常聪明地处理这件事并将很多区域聚合成一个. 在 Chrome 中这将更复杂,因为渲染器在区别于主进程的另一个进程中. Chrome 在一定程度上模仿了操作系统的行为. presentation 将会监听这些事件并将消息代理给渲染树的根节点. 这棵树将被遍历直到到达相关的渲染器. 它将重绘它自身(通常也会重绘它的后代).

    2.7.2 绘制的顺序

    CSS2 对绘制过程顺序的定义可以参考 - http://www.w3.org/TR/CSS21/zindex.html. 它事实上是元素被压入栈上下文的顺序. 顺序关系将影响绘制,因为栈从栈底向顶部绘制. 一个块级渲染器的压栈顺序是:

    1. 背景色
    2. 背景图片
    3. 边框
    4. 后代
    5. 轮廓线

    2.7.3 Firefox 显示列表

    Firefox 遍历渲染树然后构建一个用于绘制矩形的显示列表. 它包含了由正确绘制顺序(先是渲染器背景,然后是边框等等)构成的与矩形相关的渲染器.

    用这种方法重绘时树只需要遍历一次而不是多次就可以绘制所有的背景,然后是所有的图片,然后是所有的边框等等.

    Firefox 通过不添加隐藏的元素来优化这个过程,比如那些完全被不透明元素覆盖的元素.

    2.7.4 Webkit 矩形储存

    在重绘前,Webkit 将所有旧的矩形用位图储存起来. 然后它将只重绘新旧矩形之间的不同的地方.

    2.8 动态变化

    浏览器将会尝试对于变化做出最小的可行回应. 所以一个元素的颜色的干煸将只会导致重绘这一个元素. 元素位置的改变将会导致重新布局,重绘这个元素,它的后代与可能受到影响的兄弟节点. 增加一个 DOM 节点将会导致重新布局然后重绘这个节点. 大的变化,像是增加 html 元素的大小,将会导致缓存失效,整个树都将会重新布局然后重新绘制.

    2.9 渲染引擎的线程

    渲染引擎是单线程的. 几乎所有除了网络操作的事情都发生在同一个线程里. 在 Firefox 与 Safari 中这个线程就是浏览器的主线程. 在 Chrome 中它是标签页的主线程.

    网络操作可以由多个并行线程组成. 并行连接的数量受到限制(通常是2-6个连接.比如在Firefox 3中是6个).

    2.9.1 事件循环

    浏览器的主线程是一个事件循环. 这个无限的循环保持的所有过程处于激活状态. 它等待事件(比如布局或者绘制事件)然后处理它们. 这里是 Firefox 用于主事件循环的代码:

    while (!mExiting)
        NS_ProcessNextEvent(thread);
    

    2.10 CSS2 可视模型

    2.10.1 画布

    根据 CSS2 规范,画布用来描述“格式化的结构被渲染的空间”,也就是浏览器绘制内容的地方.

    画布在每个维度的空间上都是无限延伸的,但是浏览器将会根据 viewport 的大小来选择一个初始的宽度.

    根据 http://www.w3.org/TR/CSS2/zindex.html,如果被包含在另一个画布中,那么画布将是透明的,如果没有定义颜色那么将交给浏览器自己定义.

    2.10.2 CSS 盒模型

    CSS 盒模型描述了由文档树中元素生成的根据可视的格式化模型规定的矩形盒.

    每一个盒子都包含有一块内容区域(比如文本,图像等等)以及可选的内边距,边框与外边距区域.

    enter image description here
    CSS2 盒模型

    每个节点都将产生0到n个这样的盒子.

    每个元素都会有一个在它们被生成的时候决定它们盒子类型的「display」属性.比如:

    block  - generates a block box.
    inline - generates one or more inline boxes.
    none - no box is generated.
    

    默认情况下是行内属性但是浏览器样式表也设定了其他默认值. 比如div元素的默认显示是块级元素.

    你可以在这里找到默认的样式表 http://www.w3.org/TR/CSS2/sample.html.

    2.10.3 定位方案

    有三种方案:

    1. 正常(Normal) - 对象将根据它在文档中的位置定位 - 这意味着它在渲染树中的位置与在DOM 树中的位置相同,根据它的盒类型与大小来摆放.
    2. 浮动(Float)- 对象首先像正常流一样摆放,然后尽可能的移动到最左或者最右的地方.
    3. 绝对(Absolute)- 对象在渲染树中的位置与 DOM 树中的位置不同.

    定位方案由 positionfloat 两种属性设定.

    • staticrelative 将会导致正常流
    • absolutefixed 将会导致绝对定位

    static 定位中,如果没有 position 属性被定义那么将使用默认的定位方式. 在其他方案中,由作者来指定定位方案 - topbottomleftright.

    盒子摆放的方式由以下决定:

    • 盒子类型
    • 盒子大小
    • 定位方案
    • 其他信息 - 比如图片大小与屏幕大小

    2.10.4 盒子类型

    块级盒子:形成一个块 - 在浏览器窗口中有它自己的矩形区域.

    enter image description here
    块级盒子

    内联盒子:并没有自己的块 - 不过包含于其他的块中.

    enter image description here
    内联盒子

    块垂直由上到下摆放.内联水平摆放.

    enter image description here
    块级盒子与内联盒子的摆放格式

    内联盒子放在线之间或者“线盒子”之间. 线最少要与最高的盒子一样高不过可以更高,只要这些盒子由“基线”对其 - 这意味着一个元素的底部与另一个盒子底部对其. 当容器宽度不足时,行内元素将会被置于几行.这通常发生在段落中.

    enter image description here
    线

    2.10.5 定位

    2.10.5.1 相对

    相对定位像正常方式一样定位,不过相对于原来移动了一定偏差.

    enter image description here
    相对定位

    2.10.5.2 浮动

    一个浮动块会漂移到一条线的最左边或最右边. 有趣的特性是HTML中其他的盒子流将会环绕这个浮动块:

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

    将会显示成这样:

    enter image description here
    浮动

    2.10.5.3 绝对与固定

    布局将精确定义,与正常流无关.元素将不会参与到正常流当中. 它的尺寸将相对于容器. 在 fixed 中,容器是「viewport」.

    enter image description here
    绝对定位

    注意 - 固定的盒子即使文档滚动自身在屏幕上也不会动.

    2.10.5.6 分层表示

    由 CSS 中 z-index 属性指定. 它呈现了盒模型中的第三个维度,它的位置沿着 「z轴」.

    盒子将会被分入栈中(被称作栈上下文). 在每个栈中底部的元素将会被先绘制,在栈顶的元素将会更靠近用户. 在层叠的情况下会遮盖住先绘制的元素.

    这些栈通过 z-index 属性来排序. 拥有 z-index 属性的盒子将会形成一个局部栈. 「viewport」拥有外部栈.

    比如:

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

    将会得到这样的结果:

    enter image description here
    绝对定位

    尽管绿色的div相较于红色的先进栈,按照正常流红色将被首先绘制,但是红色的z-index属性值更高,所以它将在由根盒子保持的栈中被移动到更靠前的位置.

    2.11 参考来源

    1. Browser architecture
      1.1 Grosskurth, Alan. A Reference Architecture for Web Browsers. http://grosskurth.ca/papers/browser-refarch.pdf.

    2. Parsing
      2.1 Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (aka the “Dragon book”), Addison-Wesley, 1986
      2.2 Rick Jelliffe. The Bold and the Beautiful: two new drafts for HTML 5. http://broadcast.oreilly.com/2009/05/the-bold-and-the-beautiful-two.html.

    3. Firefox
      3.1 L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers. http://dbaron.org/talks/2008-11-12-faster-html-and-css/slide-6.xhtml.
      3.2 L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers(Google tech talk video). http://www.youtube.com/watch?v=a2_6bGNZ7bA.
      3.3 L. David Baron, Mozilla’s Layout Engine. http://www.mozilla.org/newlayout/doc/layout-2006-07-12/slide-6.xhtml.
      3.4 L. David Baron, Mozilla Style System Documentation. http://www.mozilla.org/newlayout/doc/style-system.html.
      3.5 Chris Waterson, Notes on HTML Reflow. http://www.mozilla.org/newlayout/doc/reflow.html.
      3.6 Chris Waterson, Gecko Overview. http://www.mozilla.org/newlayout/doc/gecko-overview.htm.
      3.7 Alexander Larsson, The life of an HTML HTTP request. https://developer.mozilla.org/en/The_life_of_an_HTML_HTTP_request.

    4. Webkit
      4.1 David Hyatt, Implementing CSS(part 1). http://weblogs.mozillazine.org/hyatt/archives/cat_safari.html.
      4.2 David Hyatt, An Overview of WebCore. http://weblogs.mozillazine.org/hyatt/WebCore/chapter2.html.
      4.3 David Hyatt, WebCore Rendering. http://webkit.org/blog/114/.
      4.4 David Hyatt, The FOUC Problem. http://webkit.org/blog/66/the-fouc-problem/.

    5. W3C Specifications
      5.1 HTML 4.01 Specification. http://www.w3.org/TR/html4/.
      5.2 HTML5 Specification. http://dev.w3.org/html5/spec/Overview.html.
      5.3 Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification. http://www.w3.org/TR/CSS2/.

    6. Browsers build instructions
      6.1 Firefox. https://developer.mozilla.org/en/Build_Documentation
      6.2 Webkit. http://webkit.org/building/build.html


    (原文写成于2009年,提及的内容可能与当前浏览器使用的技术与规范有些许差别,不过大体上流程与原理没有太大变化,可以作了解与参考用. 另外,由于该文相对较长,翻译过程中我也发现了有必要将文中的内容进行提炼并用自己的方法加以解释,所以等我有时间的时候可能会写一篇关于此文的精简版本:)

    Copyright (c) 2014-2016 Kyles Light.
    Powered by Tornado.
    鄂 ICP 备 15003296 号