HTML解析器的任务是将HTML标记解析成解析树。
HTML的词汇和语法在W3C规范中进行了定义。
HTML的定义采用了DTD(DocumentType Definition,文档类型定义)。
解析树是由DOM(Document Object Model)元素和属性节点构成的树结构,根节点是Document对象。
DOM与标记是一一对应的。
HelloWorld
上面这段代码对应的DOM树如下图:
HTML无法用常规的自上而下或自下而上的解析器进行解析,原因如下:
1) HTML允许省略某些隐式添加的标记,可以省略起始或结束标签。HTML的语法不是上下文无关的语法。
2) 浏览器可以接受一些常见的无效HTML用法。
3) 解析过程需要不断反复。在脚本标记中添加document.write,可以添加HTML标记,在解析过程中更改了输入内容。
基于上述原因,浏览器创建了自定义的解析器来解析HTML。解析算法分为标记化和树构建。标记化是词法分析过程,将输入内容解析成多个标记。HTML标记包括起始标记、结束标记、属性名称和属性值。标记生成器识别标记,传递给树构造器,然后接受下一个字符以识别下一个标记;直到输入的结束。
1 标记化算法
标记化算法的输入结果是HTML标记,使用状态机表示。状态机一共有4个状态:数据状态(Data)、标记打开状态(Tag open)、标记名称状态(Tag name)、关闭标记打开状态(Close tag open state)。
初始状态是数据状态。
当标记是处于数据状态时,
1)遇到字符
a. 接收一个a-z字符会创建“起始标记”,状态更改为“标记名称状态”,并保持到接收>字符。此期间的字符串会形成一个新的标记名称。接收到>标记后,将当前的新标记发送给树构造器,状态改回“数据状态”。
b. 接收下一个输入字符/时,会创建关闭标记打开状态,并更改为“标记名称状态”。直到接收>字符,将当前的新标记发送给树构造器,并改回“数据状态”。
2)遇到a-z字符时,会将每个字符创建成字符标记,并发送给树构造器。
举个例子:
Hello world
上述的代码片段标记化之后,发送给树构造器的顺序如下:
html开始标记、body开始标记、H字符、e字符、l字符、l字符、o字符、空格字符、w字符、o字符、r字符、l字符、d字符、body结束标记、html结束标记、文件结束标记
2 树构建算法
在创建解析器的同时,也会创建Document对象。在树构建阶段,以Document为根节点的DOM树也会不断进行修改,添加各种元素。标记生成器发送的每个节点都会由树构建器进行处理。每个标记都有对应的DOM元素,这些元素会在接收到标记时创建。
开放元素的堆栈:添加到DOM树中的元素也会添加到开放元素的堆栈中,用于纠正嵌套错误和处理未关闭的标记。
树构建算法可以用状态机来描述,称为“插入模式”。
树构建阶段的输入是一个来自标记化阶段的标记序列。以上例来说明,第一个模式是“initial mode”。接收HTML标记后转为“before html”,并在这个模式下创建一个HTMLHtmlElement元素,并将其附加到Document根对象上。状态改为“before head”。此时接收到“body”标记,系统创建一个HTMLHeadElement,并添加到树中,然后进入“in head”模式,然后转入“after head”模式。系统对body标记重新处理,创建HTMLBodyElement并插入到DOM树,模式变为“in body”。然后接收到“H”字符标记,创建Text节点。后面的“ello world”字符标记也被附加到了这个Text节点上。接收到body结束标记时,系统进入“after body”模式。在接收到html结束标记时,进入“after after body”模式。接收到文件结束标记后,解析过程结束。
在解析的过程中,浏览器会将文档标注为交互状态,并开始解析出于“deferred”模式的脚本,这些脚本是应在文档解析完成后才执行的脚本。当文档状态为“完成”时,会触发一个“加载”事件。
【参考资料】