js框架開發之旅--選擇器二

這一篇我要演示如何實現一個選擇器引擎。實現一個選擇器比想象中的要麻煩,我們會重點講那些最關鍵的技術。
要做一個好的選擇器,你必須知道瀏覽器渲染頁面的基本原理、DOM結構、CSS語法,還有瀏覽器是怎麼通過選擇器查找元素的。


CSS選擇器

CSS選擇器非常有用,他可以簡化複雜結構的選擇。解析任何東西都要先了解我們要操作的對象,我會把類庫的範圍限制在CSS2的一個子集內。
CSS2選擇器在草案中已經解釋的非常詳細了: Selectors: Pattern MatchingAppendix 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規則
  •     類規則
  •     標籤規則
  •     一般規則
選擇器的最後一部分(最右邊部分)稱爲關鍵選擇器。瀏覽器首先通過關鍵選擇器過濾出所有符合規則的元素集合,然後再通過其他規則進行過濾(從右到左)。因此我們查詢element#idName的查詢速度要比#idName慢。這種匹配方式並不是最快的,但這卻是最容易理解和最有效的方式。
爲了代碼的可維護性,我們把這些策略放到一個對象裏:
  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類創建一個全局的正則表達式
這個過程會產生大量的正則表達式,這和Sizzle及Sly使用正則表達式是類似的。這樣的好處是你可以清楚的看清選擇器和DOM匹配搜索之間的關係。


使用這些正則表達式

我們得到一個選擇器,然後通過掃描和正則表達式把它拆分成幾部分。這些工作基於匹配元素的索引:
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


牧客網--讓自由職業成爲一個靠譜的工作


發佈了16 篇原創文章 · 獲贊 87 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章