要做一個好的選擇器,你必須知道瀏覽器渲染頁面的基本原理、DOM結構、CSS語法,還有瀏覽器是怎麼通過選擇器查找元素的。
CSS選擇器
CSS選擇器非常有用,他可以簡化複雜結構的選擇。解析任何東西都要先了解我們要操作的對象,我會把類庫的範圍限制在CSS2的一個子集內。CSS2選擇器在草案中已經解釋的非常詳細了: Selectors: Pattern Matching 和Appendix G. Grammar of CSS 2.1。
我們把重點放在下面的語法上:
- E – 匹配所有的標籤名稱爲E的元素
- E F – 匹配所有E元素所屬的標籤F
- .classname – 匹配所有class屬性爲classname的所有元素
- E.class – 匹配所有class屬性爲classname的所有E標籤元素
- #id – 匹配所有id值爲id的所有元素
- E#id – 匹配所有id值爲id的所有E標籤元素
以上所有規則被稱爲簡單選擇器,簡單選擇器可以通過空格、">"和"+"等操作符連接。
解析和搜索策略
瞭解搜索策略最好的方式是通過瀏覽器。Mozilla開發者網站有一篇文章Writing Efficient CSS 解釋了樣式的匹配規則。選擇器分爲以下四種策略:
- ID規則
- 類規則
- 標籤規則
- 一般規則
爲了代碼的可維護性,我們把這些策略放到一個對象裏:
findMap = {
'id': function(root, selector) {
},
'name and id': function(root, selector) {
},
'name': function(root, selector) {
},
'class': function(root, selector) {
},
'name and class': function(root, selector) {
}
};
分詞器
分詞就是把字符分解並歸類,這個階段稱爲詞法分析,這聽着好像挺麻煩的。我們拿到一個選擇器,我們要做的是:- 刪除沒用的空白字符
- 把查詢字符串解析成我們要查詢的指令
- 在DOM上運行這些查詢指令
function Token(identity, finder) {
this.identity = identity;
this.finder = finder;
}
Token.prototype.toString = function() {
return 'identity: ' + this.identity + ', finder: ' + this.finder;
};
finder是指我們的選擇器類型,就是findMap裏定義的那些。identity是選擇器的基本規則。
掃描器
Sly和Sizzle都是使用正則表達式實現選擇器的,Sizzle把這個稱爲Chunker。Javascript正則表達式的性能和靈活性足以勝任這樣的工作,但是爲了讓讀者更清楚的瞭解其中的原理,我們將使用一個不同的方法。
大部分編程語言都提供分詞的工具。一般詞法分析就是建立在它的分詞器基礎上的。
詞法分析器用於進行編程語言的解析,如今我們生活在一個滿是計算機數據的世界裏。
你會發現,像noko這樣的項目(一個Ruby的HTML和XML解析器)已經給了開發者提供了一個詞法分析器。
詞法分析器的優勢在於,它在編程人員和解析器之間提供了一個抽象。使用這些抽象的接口比我們從頭去實現容易的多。
讓我們選擇一個極爲簡單的詞法分析器來取代正則表達式的分詞功能。這些規則基於CSS語法說明的規則描述。
我們最好把用到的匹配規則嵌入到一個對象裏,避免漏掉哪一個:
macros = {
'nl': '\n|\r\n|\r|\f',
'nonascii': '[^\0-\177]',
'unicode': '\\[0-9A-Fa-f]{1,6}(\r\n|[\s\n\r\t\f])?',
'escape': '#{unicode}|\\[^\n\r\f0-9A-Fa-f]',
'nmchar': '[_A-Za-z0-9-]|#{nonascii}|#{escape}',
'nmstart': '[_A-Za-z]|#{nonascii}|#{escape}',
'ident': '[-@]?(#{nmstart})(#{nmchar})*',
'name': '(#{nmchar})+'
};
rules = {
'id and name': '(#{ident}##{ident})',
'id': '(##{ident})',
'class': '(\\.#{ident})',
'name and class': '(#{ident}\\.#{ident})',
'element': '(#{ident})',
'pseudo class': '(:#{ident})'
};
掃描器的工作方式如下:- 在macros中展開#{}
- 在展開的macros規則基礎上展開#{}
- 編碼反斜槓符號
- 把個範式用|連接起來
- 使用RegExp類創建一個全局的正則表達式
使用這些正則表達式
我們得到一個選擇器,然後通過掃描和正則表達式把它拆分成幾部分。這些工作基於匹配元素的索引:while (match = r.exec(this.selector)) {
finder = null;
if (match[10]) {
finder = 'id';
} else if (match[1]) {
finder = 'name and id';
} else if (match[29]) {
finder = 'name';
} else if (match[15]) {
finder = 'class';
} else if (match[20]) {
finder = 'name and class';
}
this.tokens.push(new Token(match[0], finder));
}
儘管有點羅嗦,但是要比在每個正則表達式裏找match[0]要有效的多。下一篇
我們會在下一篇實現類似FireFox的搜索算法。我們讓代碼保持簡單,並且能通過大部分的瀏覽器測試如 IE6, IE7, IE8, Firefox, Safari, Chrome 和 Opera。我們要實踐基於驅動的開發,來開發我們的解析和分詞器。想要看更多的代碼,請查看GitHub,turing.dom.js