在浏览器的背后(一) —— HTML语言的词法解析
感谢老庄(@庄表伟)、耗子叔(@左耳朵耗子)、貘大(@貘吃馍香)的鞭策,使得我有勇气开始这个系列。
还有感谢@玉面小飞鱼妹纸的提问,这是我的文收到的仅有的认真回复,我一定努力快点把这系列写到布局的部分回答你的问题……
从现在开始我们来扮演浏览器。
基本知识
对我们来说HTML其实首先是一坨字符串。
嗯,考虑到我们不能等下载完成再开始解析,实际上我们要面对的是"字符流"。
为了把字符流解析成正确的DOM结构,我们需要做的事情分为两步:
- 词法分析:把字符流初步解析成我们可理解的"词",学名叫token
- 语法分析:把开始结束标签配对、属性赋值好、父子关系连接好、构成dom树
词法:状态机
html结构不算太复杂,我们需要90%的token大约只有标签开始、属性、标签结束、注释、CDATA节点。
实际上有点麻烦的是,因为HTML跟SGML的千丝万缕的联系我们需要做不少容错处理。<?和<%什么的也是必须支持好的,报了错也不能吭声。
现在我们来看看这些token都长啥样子:
- <abc
- a = "xxx"
- </xxx>
- />
- <xxx>
- hello world!
- <!-- xxx -->
- <![CDATA[hello world!]]>
根据这样的分析,现在我们开始从字符流读取字符,嗯假设是<的话,我们一下子就知道这不是一个文本节点啦!
之后再读一个字符,比如就是 x,那么一下子就知道又不是注释和CDATA了,接下来我们就一直读,直到遇到>或者空格,就得到了一个完整的token了。
那么实际上我们每读入一个字符,都要做一次决策,而且这些决定跟“当前状态”有关。这是一个典型的状态机场景。
在稍微后面的部分,可以找到状态机的状态转移图。
接下来就是代码实现的事情了,在C/C++和JS中实现状态机最棒的方式大同小异:每个函数当做一个状态,参数是接受的字符,返回值是下一个状态函数。
(这里我希望再次强调下,状态机真的是一种没有办法封装的东西,永远不要试图封装状态机。)
图上的data状态大概就像这样吧:
var data = function(c){
if(c=="&") {
return characterReferenceInData;
}
if(c=="<") {
return tagOpen;
}
else if(c=="\0") {
error();
emitToken(c);
return data;
}
else if(c==EOF) {
emitToken(EOF);
return data;
}
else {
emitToken(c);
return data;
}
};
词法分析器接受字符的方式很简单,像下面这样:
function HTMLLexicalParser(){
//状态函数们……
function data() {
// ……
}
function tagOpen() {
// ……
}
// ……
var state = data;
this.receiveInput = function(char) {
state = state(char);
}
}
接下来我们来直观地感受下(可以打开控制台来看输出):
稍微干净的代码在这个gist可以看到。
这些代码仅仅希望展示HTML的解析原理,略去了大部分的HTML状态,如果你想要完整实现HTML的词法,w3c的规范已经很贴心地把整个的状态机都给你定义好了。