在瀏覽器的背后(一) —— 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的規范已經很貼心地把整個的狀態機都給你定義好了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM