感謝老庄(@庄表偉)、耗子叔(@左耳朵耗子)、貘大(@貘吃饃香)的鞭策,使得我有勇氣開始這個系列。
還有感謝@玉面小飛魚妹紙的提問,這是我的文收到的僅有的認真回復,我一定努力快點把這系列寫到布局的部分回答你的問題……
從現在開始我們來扮演瀏覽器。
基本知識
對我們來說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的規范已經很貼心地把整個的狀態機都給你定義好了。