在浏览器的背后(一) —— 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的规范已经很贴心地把整个的状态机都给你定义好了。

posted @ 2013-05-21 18:09 winter-cn 阅读(...) 评论(...) 编辑 收藏