HTML解析过程全解

浏览器是使用最广泛的软件。目前桌面端的五大浏览器包括Chrome, IE,Firefox, Safari和Opera,移动端有安卓浏览器,iPhone, Opera Mini和Opera Mobile, UC浏览器,诺基亚S40/S60浏览器还有Chrome。这些浏览器中,除了Opera,其他均是基于WebKit引擎。

浏览器的主要功能

浏览器最主要的功能是将从服务器请求来的资源展现在浏览器窗口中。这些资源通常是一个HTML文档,但有时也会是PDF文件、图片或其他类型的内容。这些资源的来源由用户通过URI(统一资源辨识符)来明确(即用户输入网址地址)。

W3C(World Wide Web联盟)规定了关于网页的标准,其中包括针对HTML和CSS的标准。浏览器会根据这些标准来解析和呈现HTML文件。过去浏览器们只遵循部分标准,同时发展自己的插件,这导致了严重的兼容性问题,现在大部分浏览器都遵循统一的标准。

浏览器的用户界面基本一致,包括以下元素:

  • 用于输入URI的地址栏
  • 前进和后退按钮
  • 书签
  • 刷新和停止加载按钮
  • Home键用于回到主页

有意思的是,这些功能并不是官方规定必须具备的,而是浏览器为了提高用户体验,互相吸收彼此的优点,最终形成的统一的范式。


浏览器的架构

浏览器的主要组件包括:

  • **用户界面(The user interface)**:包括地址栏、前进后退按钮、书签菜单等所有除了展示请求页面的窗口之外的部分
  • **浏览器引擎(The browser engine)**:在用户界面和渲染引擎之间传递指令
  • **渲染引擎(The rendering engine)**:用来展示请求的内容。比如请求了一个HTML文件,渲染引擎就会解析HTML和CSS文件,并将最终的解析结果呈现在屏幕上
  • **网络(Networking)**:用来发送网络请求,如HTTP请求,这一接口与平台无关,能够为各种平台提供底层实现
  • **用户界面后端(UI backend)**:用于绘制基础插件如组合框、窗口等。UI后端提供了一个平台之间无差别的通用接口,而在底层使用操作系统的用户界面方法
  • **JS解释器(JavaScript interpreter)**:用于解析和执行JavaScript代码
  • 数据存储:这是一个持久层,浏览器需要在本地存储不同的数据,比如cookies。浏览器支持不同的存储机制,包括localStorage, IndexedDB, WebSQL, FileSystem。

img

值得注意的是,和大多数浏览器不同,Chrome浏览器的每个标签页都对应一个渲染引擎的实例,每个tab标签页都有一个单独的进程。


渲染引擎

渲染引擎用于将请求内容呈现到浏览器屏幕上。

渲染器引擎默认可以呈现HTML、XML文档和图片,通过插件也能呈现其他类型的数据,比如,通过PDF插件可以呈现PDF文件。

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

WebKit是一个开源渲染引擎,最开始用于Linux平台,Apple对其进行改进后用于Mac和Windows。

主流程

渲染引擎从网络层获取请求的文档内容,内容大小会限制在8KB内。之后,就开始如下流程:

image-20180906112105941

渲染引擎开始解析HTML文档,并将文档内的元素转化成DOM节点,形成content tree(内容树)。接着引擎会解析外部CSS文件和<style>元素中的样式数据,形成render tree(渲染树)。

渲染树包括多个带有视觉属性(如颜色、大小)的矩形,这些矩形按照呈现到屏幕上的顺序排列。

渲染树完成后就进入“layout(布局)”阶段。这一阶段会给每一个节点一个确切的在屏幕上的坐标。下一阶段“painting绘制”会遍历渲染树,并将每一个node节点通过UI后端层绘制出来。

需要着重提出的是,这是一个渐进的过程。想要获得更好的用户体验,渲染引擎应该越快呈现内容越好,它会在HTML被解析完毕前就开始构建渲染树和设置布局,在不断接收和处理来自网络的其余内容的同时,一部分内容已经被解析和呈现出来。

  • WebKit main flow

webkitflow

  • Gecko main flow

image008

可以看到,虽然WebKit和Gecko使用的术语略有不同,但是基本流程是一致的。


解析:综述

解析文档就是将文档内容翻译成代码能够直接使用的结构,解析的结果通常是一个能够翻译文档结构的节点树,这个树被称为“parse tree(解析树)”或“syntax tree(语法树)”。

例如解析“2+3-1”这一表达式就会返回这样一个解析树:

img

语法

解析要基于文档遵循的语法规则,如使用的语言或范式。每一种能够被解析的范式都有着明确的语法规则,包括词汇和语句规则。这被称为上下文无关文法(context free grammar)。人类语言就不符合这一类型,因此也无法使用传统的解析工具来解析。

解析器-词法分析器

解析包括两个子进程:词法分析语法分析

词法分析将输入的内容分解成**标记(token)**,标记是语言中的词汇,即构成内容的单位。在人类语言中,标记包括所有出现在字典中的字/词。

语法分析指对语言句法的应用。

解析器将解析过程分为两个模块:词法分析器用来将输入的内容分解成有效的标记,解析器根据语言的句法规则构建与文档结构对应的解析树。词法分析器会自动略过不相关的内容,如空格、换行。

img

解析是一个迭代的过程。通常,解析器会向词法分析器请求一个新的标记,并尝试将其与某条语法规则进行匹配,如果发现了匹配规则,解析器就将对应的节点添加到解析树中,然后请求下一个标记。如果没有规则可以匹配,解析器就将标记存储到内部,并继续请求标记,直到找到可与全部内部存储的标记匹配的规则。如果找不到匹配规则,解析器就会引发一个异常,这就意味着文档无效,包含语法错误。

翻译

通常,解析树都不是最终结果。解析是在翻译的过程中使用的。翻译用于将输入的文档转化成另一个范式。编译器就是一个例子,它将输入到机器中的源码先解析成一个解析树,然后将解析树翻译成机器编码文件。

image-20180906141956594

词汇和句法

词汇通常用正则表达式来表示。比如数字可以这样表达:

1
INTEGER: 0|[1-9][0-9]*

语法通常遵循BNF范式。

解析器的种类

解析器有两种:自上而下解析器自下而上解析器

自上而下解析器是从语法的高层结构触发,尝试从中找到匹配的结构;自下而上解析器是从底层规则触发,将输入内容逐步转化成语法规则,直至满足高层规则。


HTML解析器

HTML解析器用来将HTML标记解析成解析树。

非上下文无关语法

HTML解析器不能很容易地用解析器所需的上下文无关语法来定义。用来定义HTML的正规格式是DTD(Document Type Definition,文档类型定义),它不是上下文无关语法。

HTML无法使用上下文无关语法解析器的原因是它是一种“软性”语法。HTML允许省略一些隐式添加的标记,或者一些起始/结束标记等,这使得HTML语言更为“宽容”,简化了网络开发的难度,但是也使得它很难编写正式的语法。

DOM

解析器解析出的解析树是由DOM元素和属性节点共同构成的树状结构。DOM(文档对象模型)是HTML文档的对象表示,也是外部内容与HTML元素之间的接口。它的根节点是document对象。

DOM和标记之间几乎是一一对应的。以这段代码为例:

1
2
3
4
5
6
7
8
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>

可以解析成如下DOM树:

img

和HTML一样,DOM也是W3C规定的。

解析算法

HTML无法使用常规的自上而下或自下而上的解析器来解析,因为:

  • HTML语言较为宽容
  • 浏览器历来对一些常见的无效HTML用法采用包容态度
  • 解析过程要不断反复,原内容在解析过程中通常不变,但是HTML中,脚本语言会添加额外标记,改变解析内容

因此,浏览器创建了自定义的解析器来解析HTML。这一内容包括两个阶段:标记化树构建

标记化是词法分析过程,将输入内容拆解成多个标记,HTML标记包括起始标记、结束标记、属性名称和属性值

标记生成器识别标记,传递给树状构造器,然后接受下一个字符,用来识别下一个标记,如此反复直至输入结束。

img

标记生成器的算法原理可以理解为,初始状态是数据状态,遇到<符号时,状态就更高为“标记打开状态”,接收到字符会创建起始标记,状态更高为“标记名称状态”,收到>符合后,状态回到“数据状态”,如此反复来识别和生成标记。

在创建解析器的同时,也会创建document对象,在树构建阶段,以document对象为根节点的DOM树会不停修改。标记生成器发送的每一个节点都会由树创建器来处理。这些元素不仅被添加到DOM树中,还会被添加到开放的堆栈中,用于纠正嵌套错误和处理未关闭的标记。

树构建器的输入是一个来自标记化阶段的标记序列。第一个模式是”initial mode“,接到HTML标记后转为“before html”模式,创建HTMLHtmlElement元素,并添加到DOM树中;然后状态改为”before head“,此时即使没有head标记,也会隐式创建一个HTMLHeadElement添加到树中;接着进入”in head“模式,接着转入”after head“模式;再接受body标记,创建并插入HTMLBodyELement到树中,同时转为”in body“模式。在”in body“模式中不断接收字符标记,并创建和插入对应节点,最终直至文档结束标记,解析过程就此结束。

img

解析结束后的操作

解析结束后,浏览器将文档标注为交互状态,并开始解析那些处于“deferred”模式的脚本,也就是文档解析后才执行的脚本,然后文档状态将设置为“完成”,此时,load事件会被触发。


CSS解析

不同于HTML,CSS是上下文无关语法,CSS规范规定了CSS的此法和语法,所以可以使用上文中描述的各种解析器来解析。

img


处理脚本和样式表的顺序

脚本

网络模型是同步的。也就是说,解析器遇到<script>标记时,会立即解析并执行脚本。此时文档的解析会停止,直到脚本解析完成。(这也是为什么建议将文档内的script内容放到body底部,因为文档在未解析完成的情况下,先执行脚本,可能会导致错误)

如果脚本是外部的,解析过程也会停止,直到从网络同步抓取资源完成后再解析。

如果将脚本标注为”defer“,则不会停止文档解析,而是等到解析结束后再执行。

HTML5新增了一个选项,可以将脚本标注为“async”,可以由其他线程解析和执行。

defer和async的区别:

defer是延迟执行,它会在HTML解析的同时下载脚本文件,但直到HTML解析完成后才会执行脚本,看起来的效果像是将脚本放在body后面,有多个脚本defer的情况下,按照脚本顺序在文档解析完成后一次执行;

async是异步执行,它在HTML解析的同时异步下载脚本文件,下载完毕后立刻会执行,且有多个脚本文件时无法保证执行顺序。执行一定在onload前,但不保证在DOMContentLoaded事件的前或后;

IE9对defer的支持不太好

预解析

WebKit和Firefox都进行了这项优化。在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。

预解析器不会修改DOM树,只解析外部资源的引用。

样式表

理论上来说,应用样式表不会修改DOM树,因此不用等待样式表并停止文档解析。但事实上,脚本在文档解析阶段会请求样式信息,如果当时还没有加载和解析样式,脚本就会获取错误的信息,这样会产生很多问题。

Firefox在样式表加载和解析的过程中,会禁止所有脚本,WebKit在脚本尝试访问样式属性还未加载的样式表时,会禁止该脚本。


渲染树构建

渲染树是在DOM树构建的同时构建的,即HTML解析过程中,样式表解析也在同步进行。渲染树是由可视化元素按照其显示顺序组成的树,它的作用是按照正确的顺序绘制内容。

每一个渲染器都代表了一个矩形的区域,对应相应节点的CSS框,它包含了诸如宽高、位置等几何信息。框的类型会受到与节点相关的”display“样式属性的影响。

渲染树和DOM树的关系

渲染器和DOM元素是相对应的,但并非一一对应。非可视化的DOM元素不会插入到渲染树中,比如head元素,display:none的元素也不会插入到渲染树中,但是visibility:hidden元素会在渲染树中。

有一些结构复杂的元素会对应多个可视化对象,如select元素。

还有一些对象对应于DOM节点,但在树中所在位置和DOM节点不同,例如浮动定位和绝对定位的元素,它们在正常的流程之外,放置在树中的其他地方。

img

样式计算

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

样式计算有以下困难:

  • 样式数据是一个超大的结构,存储无数样式属性,困难造成内存问题;
  • 如果不进行优化,元素匹配需要为每一个元素遍历整个规则列表来匹配规则,工程浩大;
  • 应用规则设计复杂的层叠规则

布局

渲染器在创建完成时,并不包含位置和大小信息,计算这些值的过程叫做布局或重排。

HTML采用基于流的布局模型,靠后位置的元素一般不会影响到靠前位置元素的几何特种,所以大多数情况下,只要一次遍历就能计算出所需的几何信息,布局可以按从左到右、从上到下的顺序遍历文档。

坐标系是相对于根框架建立的,使用的是上坐标和左坐标。根渲染器的位置是(0,0)。

所有渲染器都有”layout(布局)”和”reflow(重排)”方法。


绘制

系统会遍历渲染树,并调用渲染器的”paint”方法,将渲染器的内容呈现在屏幕上。绘制工作是使用用户界面基础组件完成的。

绘制顺序

绘制顺序是元素进入堆栈式上下文的顺序,这些堆栈会从后往前绘制。

块渲染器的对战顺序如下:

  • 背景颜色
  • 背景图片
  • 边框
  • 子代
  • 轮廓

Summary

浏览器加载HTML文件大致过程如下:

image-20180906112105941

1. HTML解析

  • 用户在浏览器的地址栏中输入网址后,浏览器会向服务器发送请求,服务器返回文件
  • 渲染引擎开始解析HTML文件,将标记转化成DOM节点,生成DOM树
    • 标记化过程会依照<>标签依次查找HTML中的标记,发送给构建树
    • 构建树过程依照标记序列依次创建对应的元素节点,插入到DOM树中
    • 直到html结束标签,页面解析完毕
  • 如果头部引用了外部css文件,浏览器会发出请求,服务器返回该文件,这一过程会阻塞HTML解析
  • 如果引用了外部js文件,浏览器会发出文件请求,服务器返回后立即执行脚本,这个过程中,HTML解析会停止;如果头部有script代码,也会立刻执行,HTML解析会停止;如果外部js文件标明deferasync,则不会影响HTML解析

2. CSS解析

  • 浏览器会解析外部CSS文件和<style>标签中的内容,设置相应标签的样式属性,生成渲染树

3. 布局

  • 生成的渲染树不包含大小、位置信息,布局会遍历渲染树,计算大小、位置的值
  • 如果img元素音乐了图片资源,服务器返回图片文件后,也会影响到后面元素的布局,需要重新渲染这部分内容
  • 如果js脚本中使用了style.display="none",布局被改变,引擎也需要重新布局和渲染这部分内容

4. 绘制

  • 系统会遍历渲染树,调用渲染器的”paint”方法,将渲染器的内容绘制到屏幕上

参考:

How Browsers Work: Behind the scenes of modern web browsers