第七章:選擇器引擎 第六章:類工廠

jQuery憑藉選擇器風靡全球,各大框架類庫都爭先開發本身的選擇,一時間內選擇器變爲框架的標配javascript

早期的JQuery選擇器和咱們如今看到的遠不同。最初它使用混雜的xpath語法的selector。
第二代轉換爲純css的自定義僞類,(好比從xpath借鑑過來的位置僞類)的sizzle,但sizzle也一直在變,由於他的選擇器一直存在問題,一直到JQuery1.9才搞定,並最終全面支持css3的結構僞類css

2005 年,Ben Nolan的Behaviours.js 內置了聞名於世的getElementBySelector,是第一個集成事件處理,css風格的選擇器引擎與onload處理的類庫,此外,往後的霸主prototype.js頁再2005年誕生。但它勉強稱的上是,選擇器$與getElementByClassName在1.2出現,事件處理在1.3,所以,Behaviour.js還風光一時。html

本章從頭到尾實驗製造一個選擇器引擎。再次,咱們先看看前人的努力:java

1.瀏覽器內置尋找元素的方法node

請不要追問05年以前開發人員是怎麼在這種缺東缺西的環境下幹活的。那時瀏覽器大戰正酣。程序員發明navugator.userAgent檢測進行"自保"!網景戰敗,所以有關它的記錄很少。但IE確實留下很多資料,好比取得元素,咱們直接能夠根據id取得元素自身(如今全部瀏覽器都支持這個特性),不經過任何API ,自動映射全局變量,在不關注全局污染時,這是個很酷的特性。又如。取得全部元素,使用document.All,取得某一種元素的,只需作下分類,如p標籤,document.all.tags("p")。css3

有資料可查的是 getElementById , getElementByTagName是ie5引入的。那是1999年的事情,伴隨一個輝煌的產品,window98,捆綁在一塊兒,所以,那時候ie都傾向於爲IE作兼容。程序員

(感興趣的話參見讓ie4支持getElementById的代碼,此外,還有getElementByTagsName的實現)正則表達式

但人們很快發現問並沒有法選取題了,就是IE的getElementById是不區分表單元素的ID和name,若是一個表單元素只定義name並與咱們的目標元素同名,且咱們的目標元素在它的後面,那麼就會選錯元素,這個問題一直延續到ie7.算法

IE下的getElementsByTagesName也有問題。當參數爲*號通配符時,它會混入註釋節點,並沒有法選取Object下的元素。chrome

(解決辦法略去)

此外,w3c還提供了一個getElementByName的方法,這個IE也有問題,它只能選取表單元素。

在Prototype.js還未到來以前,全部可用的只有原生選擇器。所以,simon willson高出getElementBySelector,讓世人眼前一亮。

以後的過程就是N個版本的getElementBySlelector,不過大多數是在simon的基礎上改進的,甚至還討論將它標準化!

getElementBySlelector表明的是歷史的前進。JQuery在此時優勢偏向了,prototype.js則在Ajax熱浪中扶搖直上。不過,JQuery仍是勝利了,sizzle的設計很特別,各類優化別出心裁。


Netscape藉助firefox還魂,在html引入xml的xpath,其API爲document.evaluate.加之不少的版本及語法複雜,所以沒有普及開來。

微軟爲保住ie佔有率,在ie8上加入querySelector與querySlectorAll,至關於getElementBySelector的升級版,它還支持史無前例的僞類,狀態僞類。語言僞類和取反僞類。此時,chrome參戰,激發瀏覽器標準的熱情和升級,ie8加入的選擇器你們都支持了,還支持的更加標準。此時,還出現了一種相似選擇器的匹配器————matchSelector,它對咱們編寫選擇器引擎特別有幫助,因爲是版本號競賽時誕生的,誰也不能保證本身被w3c採納,都帶有私有前綴。如今css方面的Selector4正在起草中,querySeletorAll也只支持到selector3部分,但其間兼容性問題已經很雜亂了。

2.getElementsBySelector

讓咱們先看一下最古老的選擇器引擎。它規定了許多選擇器發展的方向。在解讀中能涉及到不少概念,但沒關係,後面有更詳細的解釋。如今只是初步瞭解下大概藍圖。

/* document.getElementsBySelector(selector)
    version 0.4 simon willson march 25th 2003
    -- work in phonix0.5 mozilla1.3 opera7 ie6 
    */
    function getAllchildren(e){
        //取得一個元素的子孫,併兼容ie5
        return e.all ? e.all : e.getElementsByTgaName('*');
    }

    document.getElementsBySelector = function(selector){
        //若是不支持getElementsByTagName 則直接返回空數組
        if (!document.getElementsByTgaName) {
            return new Array();
        }

        //切割CSS選擇符,分解一個個單元格(每一個單元可能表明一個或多個選擇器,好比p.aaa則由標籤選擇器和類選擇器組成)
        var tokens = selector.split(' ');
        var currentContext = new Array(document);
        //從左至右檢測每一個單元,換言此引擎是自頂向下選擇元素
        //若是集合中間爲空,當即中至此循環
        for (var i = 0 ; i < tokens.length; i++) {
            //去掉兩邊的空白(並非全部的空白都沒有用,兩個選擇器組之間的空白表明着後代迭代器,這要看做者們的各顯神通)
            token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');
            //若是包含ID選擇器,這裏略顯粗糙,由於它可能在引號裏邊。此選擇器支持到屬性選擇器,則表明着多是屬性值的一部分。
            if (token.indexOf('#') > -1) {
                //假設這個選擇器是以tag#id或#id的形式,可能致使bug(但這些暫且不談,沿着做者的思路看下去)
                var bits =token.split('#');
                var tagName = bits[0];
                var id = bits[1];
                //先用id值取得元素,而後斷定元素的tagName是否等於上面的tagName
                //此處有一個不嚴謹的地方,element可能爲null,會引起異常
                var element = document.getElementById(id);
                if(tagName && element.nodeName.toLowerCase() != tagName) {
                    //沒有直接返回空結合集
                    return new Array();
                }

                //置換currentContext,跳至下一個選擇器組
                currentContext = new Array(element);
                continue;
            }
            //若是包含類選擇器,這裏也假設它以.class或tag.class的形式
            if (token.indexOf('.') > -1){
                var bits = token.split('.');
                var tagName = bits[0];
                var className = bits[1];
                if (!tagName){
                    tagName = '*';
                }
                //從多個父節點,取得它們的全部子孫
                //這裏的父節點即包含在currentContext的元素節點或文檔對象
                var found = new Array;//這裏是過濾集合,經過檢測它們的className決定去留
                var foundCount = 0;
                for (var h = 0; h < currentContext.length; h++){
                    var elements;
                    if(tagName == '*'){
                        elements = getAllchildren(currentContext[h]);
                    } else {
                        elements = currentContext[h].getElementsByTgaName(tagName);
                    }
                    for (var j = 0; j < elements.length; j++) {
                        found[foundCount++] = elements[j];
                    }
                }

                currentContext = new Array;
                for (var k = 0; k < found.length; k++) {
                    //found[k].className可能爲空,所以不失爲一種優化手段,但new regExp放在//外圍更適合
                    if (found[k].className && found[k].className.match(new RegExp('\\b'+className+'\\b'))){
                        currentContext[currentContextIndex++] = found[k];
                    }
                }
                continue;
            }
            //若是是以tag[attr(~|^$*)=val]或[attr(~|^$*)=val]的組合形式
            if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)){
                var tagName = RegExp.$1;
                var attrName = RegExp.$2;
                var attrOperator = RegExp.$3;
                var attrValue = RegExp.$4;
                if (!tagName){
                    tagName = '*';
                }
                //這裏的邏輯以上面的class部分類似,其實應該抽取成一個獨立的函數
                var found = new Array;
                var foundCount = 0;
                for (var h = 0; h < currentContext.length; h++){
                    var elements;
                    if (tagName == '*') {
                        elements = getAllchildren(currentContext[h]);
                    } else {
                        elements = currentContext[h].getElementsByTagName(tagName);
                    }
                    for (var j = 0; j < elements.length; j++) {
                        found[foundCount++] = elements[j];
                    }
                }

                currentContext = new Array;
                var currentContextIndex = 0;
                var checkFunction;
                //根據第二個操做符生成檢測函數,後面的章節有詳細介紹 ,請繼續關注哈
                switch (attrOperator) {
                    case '=' : //
                    checkFunction = function(e){ return (e.getAttribute(attrName) == attrValue);};
                    break;
                    case '~' :
                    checkFunction = function(e){return (e.getAttribute(attrName).match(new RegExp('\\b' +attrValue+ '\\b')));};
                    break;
                    case '|' :
                    checkFunction = function(e){ return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?')));};
                    break;
                    case '^' : 
                    checkFunction = function(e) {return (e.getAttribute(attrName).indexOf(attrValue) == 0);};
                    break;
                    case '$':
                    checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length);};
                    break;
                    case '*':
                    checkFunction  = function(e) {return (e.getAttribute(attrName).indexOf(attrValue) > -1 );}
                    break;
                    default :
                    checkFunction = function(e) {return e.getAttribute(attrName);}; 
                }
                currentContext = new Array;
                var currentContextIndex = 0 ;
                for (var k = 0; k < found.length; k++) {
                    if (checkFunction(found[k])) {
                        currentContext[currentContextIndex++] = found[k];
                    }
                }
                continue;
            }
            //若是沒有 # . [ 這樣的特殊字符,咱們就當是tagName
            var tagName = token;
            var found = new Array;
            var foundCount = 0;
            for (var h = 0; h < currentContext.length; h++) {
                var elements = currentContext[h].getElementsByTgaName(tagName);
                for (var j = 0; j < elements.length; j++) {
                    found[foundCount++] = elements[j];
                }
            }
            currentContext = found;
        }
        return currentContext; //返回最後的選集
    }

 顯然當時受網速限制,頁面不會很大,也不可能有很複雜的交互,所以javascript尚未到大規模使用的階段,咱們看到當時的庫頁不怎麼重視全局污染,也不支持並聯選擇器,要求每一個選擇器組不能超過兩個,不然報錯。換言之,它們只對下面的形式CSS表達式有效:

    #aa p.bbb [ccc=ddd]

Css表達符將以空白分隔成多個選擇器組,每一個選擇器不能超過兩種選取類型,而且其中之一爲標籤選擇器

要求比較嚴格,文檔也沒有說明,所以很糟糕。但對當時編程環境來講,已是喜出望外了。做爲早期的選擇器,它也沒有想之後那樣對結果集進行去重,把元素逐個按照文檔出現的順序進行排序,咱們在第一節指出的bug,頁沒有進行規避,多是受當時javascript技術交流太少。這些都是咱們要改進的地方。

3.選擇器引擎涉及的知識點

本小節咱們學習上小節的大力的概念,其中,有關選擇器引擎實現的概念大多數是從sizzle中抽取出來的,兒CSS表達符部分則是W3C提供的,首先從CSS表達符部分介紹。

h1 {color: red;font-size: 14px;}

其中,h1 爲選擇符,color和font-size爲屬性,red和14px爲值,兩組color: red和font-size: 14px;爲它們的聲明。

上面的只是理想狀況,重構成員交給咱們CSS文件,裏邊的選擇符但是複雜多了。選擇符混雜着大量的標記,能夠分割爲更細的單元。總的來講,分爲四大類十七種。此外,還包含選擇引擎沒法操做僞元素

四大類:指並聯選擇器、 簡單選擇器 、 關係選擇器 、 僞類

並聯選擇器:就是「,」,一種不是選擇器的選擇器,用於合併多個分組的結果

關係選擇器 分四種: 親子 後代 相鄰,通配符

僞類分爲六種: 動做僞類, 目標僞類, 語言僞類, 狀態僞類, 結構僞類, 取得反僞類。

簡單的選擇器又稱爲基本選擇器,這是在prototype.js以前的選擇器都已經支持的選擇器類型。不過在css上,ie7纔開始支持部分屬性選擇器。其中,它們設計的很是整齊劃一,咱們能夠經過它的一個字符決定它們的類型。好比id選擇器的第一個字符爲#,類選擇器爲. ,屬性選擇器爲[ ,通配符選擇器爲 * ;標籤選擇器爲英文字母。你能夠能夠解釋爲何沒有特殊符號。jQuery就是使用/isTag = !/\W/.test( part )進行斷定的

在實現上,咱們在這裏有不少原生的API可使用,如getElementById. getElementsByTagName. getElementsByClassName. document.all 屬性選擇器能夠用getAttribute 、 getAttributeNode attributes, hasAttribute,2003年曾經討論引入getElementByAttribute,但沒成功,實際上,firefix上的XUI的同名就是當時的產物。不過屬性選擇器的確比較複雜,歷史上他是分爲兩步實現的。

css2.1中,屬性選擇器又如下四種狀態。

[att]:選取設置了att屬性的元素,無論設定值是什麼。
[att=val]:選取了全部att屬性的值徹底等於val的元素。
[att~=val]:表示一個元素擁有屬性att,而且該屬性還有空格分割的一組值,其中之一爲'val'。這個你們應該能聯想到類名,若是瀏覽器不支持getElementsByClassName,在過濾階段,咱們能夠將.aaa轉換爲[class~=aaa]來處理
[att|=val]:選取一個元素擁有屬性att,而且該屬性含'val'或以'val-'開頭

Css3中,屬性選擇器又增長三種形態:
[att^=val]:選取全部att屬性的值以val開頭的元素
[att$=val]:選取全部att屬性的值以val結尾的元素
[att*=val]:選取全部att屬性的值包含val字樣的元素。
以上三者,咱們均可以經過indexOf輕鬆實現。

此外,大多選取器引擎,還實現了一種[att!=val]的自定義屬性選擇器。意思很簡單,選取全部att屬性不等於val的元素,着正好與[att=val]相反。這個咱們也能夠經過css3的去反僞類實現。

咱們再看看關係選擇器。關係選擇器是不能單獨存在的,它必須在其餘兩類選擇器組合使用,在CSS裏,它必須夾在它們中間,但選擇器引擎可能容許放在開始。在很長時間內,只存在後代選擇器(E F),就在兩個選擇器E與F之間的空白。css2.1又增長了兩個,親子選擇器(E > F)相鄰選取(E + F),它們也夾在兩個簡單選擇器之間,但容許大於號或加號兩邊存在空白,這時,空白就不是表示後代選擇器。CSS3又增長了一個,兄長選擇器(E ~ F),規則同上。CSS4又增長了一個父親選取器,不過其規則一直在變化。

後代選擇器:一般咱們在引擎內構建一個getAll的函數,要求傳入一個文檔對象或元素節點取得其子孫。這裏要特別注意IE下的document.all,getElementByTagName  的("*")混入註釋節點的問題

親子選擇器:這個咱們若是不打算兼容XML,直接使用children就行。不過在IE5-8它都會混入註釋節點。下面是兼容列狀況。

chrome :1+   firefox:3.5+   ie:5+  opera: 10+  safari: 4+  

    function getChildren(el) {
        if (el.childElementCount) {
            return [].slice.call(el.children);
        }
        var ret = [];
        for (var node = el.firstChild; node; node = node.nextSibling) {
            node.nodeType == 1 && ret.push(node);
        }
        return ret;
    }

相鄰選擇器: 就是取得當前元素向右的一個元素節點,視狀況使用nextSibling或nextElementSibling.

    function getNext (el) {
        if ("nextElementSibling" in el) {
            return el.nextElementSibling
        }
        while (el = el.nextSibling) {
            if (el.nodeType === 1) {
                return el;
            }
        }
        return null
    }

兄長選擇器:就是取其右邊的全部同級元素節點。

    function getPrev(el) {
        if ("previousElementSibling" in el) {
            return el.previousElementSibling;
        }
        while (el = el.previousSibling) {
            if (el.nodeType === 1) {
                return el;
            }
        }
        return null;
    }

上面提到的childElementCount 、 nextElementSibling是08年12月經過Element Traversal規範的,用於遍歷元素節點。加上後來補充的parentElement,咱們查找元素就很是方便。以下表

查找元素
  遍歷全部子節點 遍歷全部子元素
第一個 firstChild firstElementChild
最後一個 lastChild lastElementChild
前面的 previousSibling previousElementSibling
後面的 nextSibling nextElementSibling
父節點 parentNode parentElement
數量   length childElementCount

僞類

僞類是選擇器家族中最龐大的家族,從css1開始,以字符串開頭,到css3時代,出現了要求傳參的機構僞類和去反僞類。

(1).動做僞類

動做僞類又分爲連接僞類用戶行爲僞類,其中,連接僞類由:visted和:link組成用戶行爲僞類分爲:hover,:active, :focus組成。這這裏咱們基本上只能模擬:link,而在瀏覽器的原生的querySeletorAll對它們的支持也存在差別,ie8-ie10存在取:link錯誤,它只能取a的標籤,實際:link指代a aera link這三種標籤,這個其它標籤瀏覽器都正確。另外,opera,safari外,其它瀏覽器取:focus都正常,除opera外,其它瀏覽器取得:hover都正確。剩下:active和:visted都正確。剩下的:active與visted都爲零。

window.onload = function(){
    document.querySelector("#aaa").onclick = function() {
        alert(document.querySelectorAll(":focus").length) ;// =>1
    }

    document.querySelector("#bbb").onclick = function() {
        alert(document.querySelectorAll(":hover").length); //=> 4  //4 ,html body p a
    }

    function test() {
        alert(document.querySelectorAll(":link").length);//=> 3
    }
}    

僞類沒有專門的api獲得結果集合,所以,咱們須要經過上一次獲得的結果集就行過濾。在瀏覽器中,咱們能夠經過document.links獲得部分結果,所以不包含link標籤。所以,最好的方法是斷定它的tagName是否等於A,LINK,AREA中的其中一個

(2).目標僞類

目標僞類即:target僞類,指其id或者name屬性與url的hash部分(#以後的部分),匹配上的元素
假如一個文檔,其id爲section_2,而url中的hash部分也是#section_2,那麼它就是咱們要取的元素。

Sizzle中過濾的函數以下:

    "target": function(elem) {
        var hash = window.location && window.location.hash;
        return hash && hash.slice(1) === elem.id;
    }

(3).語言僞類

語言僞類即:length僞類,用來設置使用特殊語言的內容樣式,如:lang(de)的內部應該爲德語,須要特殊處理。

注意:lang 雖然爲DOM元素的一個屬性,但:lang僞類與屬性選擇器有所不一樣,具體表現:lang僞類具備「繼承性」,以下面的html表示的文檔

<html>
<head>
</head>
<body lang="de">
<p>一個段落</p>
</body>
</html>

若是使用[lang=de]則只能選擇到body元素,由於p元素沒有lang屬性,可是使用:lang(de)則能夠同時選擇到body和p元素,表現出繼承性。

    "lang": markFunction(function(lang) {
        //lang value must be a valid iddentifider
        if (!ridentifier,test(lang || "") + lang);
    }
    lang = lang.replace(runescape, funescape).toLowerCase();
    return function(elem) {
        var ememLang;
        do {
            if ((ememLang = documentIsXML ? elem.getAttribute("xml:lang") || elem.getAttribute("lang"):elem.lang)){
                elemLang = elemLang.toLowerCase();
                return elemLang === lang || elemLang.indexOf(lang + "-") === 0;
            }
        } while ((elem = elem.parentNode) && elem.nodeType === 1);
        return false;
       }
    }),

(4).狀態僞類

狀態僞類用於標記一個UI元素的當前狀態,有:checked , :enabled , :disable 和 :indeterminate這四個僞類組成。咱們能夠分別經過元素的checked , disabled , indeteminate屬性進行斷定

 (5).結構僞類

它又能夠分爲三種根僞類子元素過濾僞類空僞類
根僞類是由它在文檔的位置斷定子元素過濾僞類是根據它在其父親的全部孩子的位置或標籤類型斷定空僞類是根據它孩子的個數斷定

:root僞類 用於選取根元素,在html文檔中,一般是html元素。

:nth-child 是全部子元素的過濾僞藍本,其它8種都是由它衍生出來的。它帶有參數,能夠是純數字,代數式或單詞,若是是數字,則從1計起,若是是代數式,n則從0遞增,很是很差理解的規則。

:nth-child(2)選取當前父節點的第2個子元素

:nth-child(n+4) 選取大於等於4的的標籤,咱們能夠將n當作自增量(0 <= n <= parent.children.length),此代數的值因變量。

:nth-child(-n+4)選取小於等於4標籤

:nth-child(2n)選取偶數標籤,2n也能夠是even

:nth-child(2n-1)選取奇數標籤,2n-1也能夠是odd

:nth-child(3n+1)表示沒三個爲一組,選取它的第一個

:nth-last-child與:nth-child差很少,不過是從後面取起來。好比:nth-last-child(2)

:nth-of-type和nth-last-of-type與:nth-child和nth-last-child相似,規則是將當前元素的父節點的全部元素按照tagName分組,只要其參數符合它在那一組的位置就被匹配到。好比:nth-of-type(2),另一例子,nth-of-type(even).

:frist-child用於選取第一個子元素,效果等同於:nth-child(1)

:last-child用於選取最後一個元素,效果等同於:nth-last-child(1)

:only-child用於選擇惟一的子元素,當子元素的個數超過1時,選擇器失效。

:only-of-type將父節點的元素按照tagName分組,若是某一組只有一個元素,那麼就選擇這些元素返回。

:empty 用於選擇那些不包含任何元素的節點,文本節點,CDATA節點的元素,但容許裏邊只存在註釋節點

Sizzle中:empty的實現以下:

    "empty": function(elem) {
        for (elem = elem.firstChild; elem ; elem = elem.nextSibling) {
            if (elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4){
                return false;
            }
        }
        return true;
    },

mootools中的Slick.實現以下。

    "empty": function(node) {
        var child = node.firstChild;
        return !(child && child.nodeType == 1) && !(node.innerText || node.textContent || '').length;
    },

(6).去反僞類

去反僞類即:not僞類,其參數爲一個或多簡單選擇器,裏邊用逗號隔開。在jQuery等選擇器引擎中容許你傳入其它類型的選擇器,甚至能夠進行多個去反僞類嵌套。

(7).引擎實現時涉及的概念

種子集:或者叫候選集,若是css選擇符很是複雜,咱們要分幾步才能獲得咱們想要的元素。那麼第一次獲得的元素集合就叫種子集。在Sizzle中,這樣基本從右到左,它的種子集中就有一部分爲咱們最後獲得的元素。若是選擇器引擎是從左至右的選擇,那麼它們只是咱們繼續查找它的子元素或兄弟的「據點」而已。

結果集:選擇器引擎最終返回的元素集合們如今約定俗成,它要保持與querySlectorAll獲得的結果一致,即,沒有重複元素。元素要按照他們在DOM樹上出現的順序排序。

過濾集:咱們選取一組元素後,它以後每個步驟要處理的元素集合均可以稱爲過濾集。好比p.aaa,若是瀏覽器不支持querySelectorAll,若支持getElementByClassName,那麼咱們就用它獲得種子集,而後經過className進行過濾。顯然在大多數狀況下,前者比後者快多了。同理,若是存在ID選擇器,因爲ID在一個文檔中不重複,所以,使用ID查找更快。在Sizzle下若是不支持QuerySlectorAll,它會只能地以ID,class,Tag順序去查找。

選擇器羣組:一個選擇符被並聯選擇器","劃分紅的每個大組

選擇器組:一個選擇器羣組被關係選擇器劃分爲的第一個小分組。考慮到性能,每個小分組都建議加上tagName,所以這樣在IE6方便使用documentsByTagName,好比p div.aaa 比 p.aaa快多了。前者兩次getElementsByTagName查找,最後用className過濾。後者是經過getElementsByTagName獲得種子集,而後再取它們的全部子孫,明顯這樣獲得的過濾集比前者數量多不少。

從實現上,你能夠從左至右,也能夠像sizzle那樣從右至左(大致上是,實際狀況複雜不少)。

另外,選擇器也分爲編譯型和非編譯型編譯型是Ext發明的,這個陣營的選擇器中有Ext,Qwrap,NWMatchers,JindoJS.非編譯型的就更多了,如Sizzle,Icarus,Slick,YUI,dojo,uupaa,peppy...

還有一些利用xpath實現的選擇器,最著名的是base2,它先實現了xpath那一套,方便IE也是有document,evaluate,而後將css選擇符翻譯成xpath。其它比較著名的有casperjs,DOMAssistant.

像sizzle mootools Icarus等還支持選擇XML元素(由於XML仍是一種比較重要的數據傳輸格式。後端經過XHR返回咱們的就多是XML),這樣咱們經過選擇器引擎抽取所須要的數據就簡單多了。

4.選擇器引擎涉及的通用函數

1. isXML

最強大的前幾名選擇器引擎都能操做XML文檔,但XML與HTMl存在很大的差別,沒有className,getElementById,而且nodeName須要區分大小寫,在舊版IE中還不能直接給XML元素添加自定義屬性。所以,這些區分很是有必要。所以咱們看一下各大引擎的實現吧。

Sizzle的實現以下。

    var isXML = Sizzle.isXML = function (elem) {
        var documentElement = elem && (elem.ownDocument || elem).documentElement;
        return documentElement ? documentElement.nodeName !== "HTML" : false;
    };

但這樣作不嚴謹,由於XML的根節點多是HTML標籤,好比這樣建立一個XML文檔:

    try{
        var doc = document.implementation.createDocument(null, 'HTML', null);
        alert(doc.documentElement)
        alert(isXML(doc))
    } catch (e) {
        alert("不支持creatDocument")
    }

咱們來看看mootools的slick的實現

    isXML = function(document){
        return (!!document.xmlVersion) || (!!document.xml) || (toString.call(document) == '[object XMLDocument]') || (document.nodeType == 9 && document.documentElement.nodeName != 'HTML');
    };

mootools用到了大量的屬性來進行斷定,從mootools1.2到如今還沒什麼改動,說明仍是很穩定的。咱們再精簡一下。

標準瀏覽器裏,暴露了一個建立HTML文檔的構造器HTMLDocument,而IE下的XML元素又擁有selectNodes:

    var isXML = window.HTMLDocument ? function(doc) {
        return !(doc instanceof HTMLDocument)
    } : function (doc) {
        return "selectNodes" in doc
    }

不過這些方法都是規範,javascript對象能夠隨意添加,屬性法很容易被攻破,最好使用功能法。不管XML或HTML文檔都支持createElement方法,咱們斷定建立了元素的nodeName是否區分大小寫。

    var isXML = function(doc) {
        return doc.createDocument("p").nodeName !== doc.createDocument("p").nodeName;
    }

這是目前能給出最嚴謹的函數了

2.contains

contains方法就是斷定參數1是否包含參數2。這一般用於優化。好比早期的Sizzle,對於#aaa p.class選擇符,它會優先用getElementByClassName或getElementsByTagName取種子集,而後就不繼續往左走了,直接跑到最左的#aaa,取得#aaa元素,而後經過contains方法進行過濾。隨着Sizzle體積進行增大,它如今只剩下另外一個關於ID的用法,即:若是上下文對象非文檔對象,那麼它會取得其ownerDocument,這樣就能夠用getElementById,而後用contains方法進行驗證!

    //sizzle 1.10.15
    if (context.ownerDocument && (elem = context.ownerDocument.getElementById(m)) && contains(context, elem) && elem.id === m) {
        results.push(elem);
        return results;
    }

contains的實現。

        var rnative = /^[^]+\{\s*\[native \w/,
        hasCompare = rnative.test( docElem.compareDocumentPosition ),
        contains = hasCompare || rnative.test(docElem.contains) ?
                   function(a, b){
                       var adown = a.nodeType === 9 ? a.documentElement : a,
                             bup = b && b.parentNode;
                       return a === bup || !!(bup && bup.nodeType === 1 && (
                           adown.contains ?
                           adown.contains(bup) :
                           a.compareDocumentPosition && a.compareDocumentPosition(bup) & 16
                           ));
                   } :
        function (a, b) {
            if (b) {
                while ((b = b.parentNode)) {
                    if (b === a) {
                        return true
                    }
                }
            }
            return false;
        };

它本身作了預斷定,但這時傳入xml元素節點,可能就會出錯,所以建議改爲實時斷定。雖然每次都進入都斷定一次那個原生API。

mass framework的實現方式:

        //第一個節點是否包含第二個節點,same容許二者相等
        if (a === b){
            return !!same;
        }
        if (!b.parentNode)
            return false;
        if (a.contains) {
            return a.contains(b);
        } else if (a.compareDocumentPosition) {
            return !!(a.compareDocumentPosition(b) & 16);
        }

        while ((b = b.parentNode))
            if (a === b) return true;
        return false;
    }

如今來解釋一下contanscompareDocumentPosition這兩個API。contains原來是IE私有的,後來其餘瀏覽器也借鑑這方法。如fireFox在9.0也安裝了此方法。它是一個元素節點的方法,若是另外一個等於或包含它的內部,就返回true.compareDocumentPosition是DOM的level3 specification定義的方法,firefox等標準瀏覽器都支持,它等於斷定兩個節點的關係,而不可是包含關係這裏是NodeA.compareDocumentPosition(Node.B)包含你能夠獲得的信息

Bits    
000000 0 元素一致
000001 1 節點在不一樣的文檔(或者一個在文檔以外)
000010 2 節點B在節點A以前
000100 4 節點A在節點B以前
001000 8 節點B包含節點A
010000 16 節點A包含節點B
100000 32 瀏覽器的私有使用

有時候,兩個元素的位置關係可能連續知足上表的二者狀況,好比A包含B,而且A在B的前面,那麼compareDocumentPosition就返回20.

舊版本的IE不支持compareDocumentPosition。jQuery做者john resig寫了個兼容函數,用到IE的另外一個私有實現,sourceIndex, sourceIndex會根據元素位置的從上到下,從左至右依次加1,好比HTML標籤的sourceIndex爲0,Head的標籤爲1,Body的標籤爲2,Head的第一個子元素爲3.若是元素不在DOM樹,那麼返回-1.

    function comparePosition(a, b) {
        return a.compareDocumentPosition ? a.compareDocumentPosition(b) :
        a.contains ? (a != b && a.contains(b) && 16) +
          (a.sourceIndex >= 0 && b.sourceIndex >= 0 ?
          (a.sourceIndex < b.sourceIndex && 4) +
          (a.sourceIndex > b.sourceIndex && 2): 1) : 0;
    }

3.節點的排序與去重

爲了讓選擇器引擎搜到的結果集儘量接近原生的API結果(由於在最新的瀏覽器中,咱們可能只使用querySlelectorAll實現),咱們須要讓元素節點按它們在DOM樹出現的順序排序

IE早期的版本,咱們可使用sourceIndex進行排序。

標準的瀏覽器可使用compareDocumentPosition.在上小節中介紹了它能夠斷定兩個節點的位置關係。咱們只要將它們的結果按位於4不等於0就知道其先後順序了

    var compare =comparerange.compareBoundaryPoints(how, sourceRange);

compare:返回1,0,-1(0爲相等,1爲compareRange在sourceRange以後,-1爲compareRange在sourceRange以前)

how:比較那些邊界點:爲常數

    1. Range.START_TO_START 比較兩個Range節點的開始點
    2. Range.END_TO_END 比較兩個Range節點的結束點
    3. Range.START_TO_END 用sourceRange的開始點與當前範圍的結束點比較
    4. Range.END_TO_START 用sourceRange的結束點與當前範圍的開始點作比較

特別的狀況發生於要兼容舊版本標準瀏覽器與XML文檔時,這時只有一些很基礎的DOM API,咱們須要使用nextSibling來斷定誰是哥哥,誰是「弟弟」。若是他們不是同一個父節點,咱們就須要將問題轉化爲求最近公共祖先,斷定誰是「父親」,誰是"伯父"節點。

到這裏,已是很單純的算法問題了。實現的思路有不少,最直觀最笨的作法是,不斷向上獲取他們的父節點,直到HTML元素,連同最初的那個節點,組成兩個數組,而後每次取數組最後的元素進行比較,若是相同就去掉。一直取到不相同爲止。最後用nextSibling結束。下面是測試的代碼。須要本身去試驗

window.onload = function () {
    function shuffle(a) {
        //洗牌
        var array = a.concat();
        var i = array.length;
        while (i) {
            var j = Math.floor(Math.random() * i);
            var t = array[--i];
            array[i] = array[j];
            array[j] = t;
        }
        return array;
    }
    var log = function(s) {
        //查看調試消息
        window.console && window.console.log(s)
    }

    var sliceNodes = function(arr){
        //將NodeList轉化爲純數組
        var ret = [],
               i = arr.length;
        while (i)
            ret [--i] = arr[i];
        return ret;
    }

    var sortNodes = function(a, b) {
        //節點排序
        var p = "parentNode",
              ap = a[p],
              bp = b[p];
        if (a === b) {
            return 0
        } else if (ap === bp) {
            while (a = a.nextSibling) {
                if (a === b) {
                    return -1
                }
            }
            return 1
        } else if (!ap) {
            return -1
        } else if (!bp) {
            return 1
        }
        var al = [],
            ap = a
        //不斷往上取,一值取到HTML
        while (ap && ap.nodeType === 1) {
            al[al.length] = ap
            ap = ap[p]
        }
        var bl =[],
            bp = b;
        while (bp && bp.nodeType === 1) {
            bl[bl.length] = bp
            bp = bp[p]
        }
        //而後一塊兒去掉公共祖先
        ap = al.pop();
        bp = bl.pop();
        while(ap === bp) {
            ap = al.pop();
            bp = bl.pop();
        }
        if (ap && bp) {
            while (ap = ap.nextSibling) {
                if (ap === bp) {
                    return -1
                }
            }
            return 1;
        }
    return ap ? 1 : -1    
    }
    var els = document.getElementsByTagName("*")
    els = sliceNodes(els); //轉換成純數組
    log(els);

    els = shuffle(els); //洗牌的過程(模擬選擇器引擎最初獲得的結果集的狀況)
    log(els);
    els = els.sort(sortNodes); //進行節點排序
    log(els)
}

 它沒打算支持xml與舊版標準瀏覽器,不支持就不會排序。

mass Framework的icarus引擎,結合了一位編程高手JK的算法,在排序去重遠勝Sizzle。

其特色在於,不管sizzle或者slick,它們都是經過傳入比較函數進行排序。而數組的原生sort方法,當它傳一個比較函數時,無論它內部用哪一種排序算法(ecma沒有規定sort的具體實現方法,所以,各個瀏覽器而異,好比FF2使用堆排序,FF3使用歸併排序,IE比較慢,具體的算法不明,可能爲冒泡或插入排序,而chrome爲了最大效率,採用了二者算法:

http://yiminghe.iteye.com/blog/469713(玉伯大神) 附加一個排序方法:http://runjs.cn/code/tam0czbv

),都須要屢次比對,因此很是耗時間,若是能設計讓排序在不傳參的狀況下進行,那麼速度就會提升N倍。

下面是具體的思路(固然只能用於IE或早期的Opeara,因此代碼不貼出來。)

i.取出元素節點的sourceIndex值,轉換爲一個String對象
ii.將元素節點附在String對象上
iii.用String對象組成數組
iiii.用原生的sor進行string對象數組排序
iiiii.在排序好的String數組中,排序取出元素節點,便可獲得排序好的結果集。

在這裏貼兩篇關於排序和節點選擇的前輩文章:擴展閱讀
http://www.cnblogs.com/jkisjk/archive/2011/01/28/array_quickly_sortby.html
http://www.cnblogs.com/jkisjk/archive/2011/01/28/1946936.html

擴展閱讀 (參考司徒先生的多代選擇器引擎:)http://www.cnblogs.com/rubylouvre/archive/2011/11/10/2243838.html

4.切割器

選擇器下降了javascript的入行門檻,它們在選擇元素時都很隨意,一級一級地向上加ID類名,致使選擇符很是長,所以,若是不支持querySlelectorAll,沒有一個原生API能承擔這份工做,所以,咱們經過使用正經常使用戶對選擇符進行切割,這個步奏有點像編譯原理的詞法分析,拆分出有用的符號法來。

這裏就拿Icarus的切割器來舉例,看它是怎麼一步步優化,就知道這工做須要多少細緻。

     /[\w\u00a1-\uFFFF][\w\u00a1-\uFFFF-]*|[#.:\[][\w\(\)\]]+|\s*[>+~,*]\s*|\s+/g

好比,對於".td1,div a,body"上面的正則可完美將它分解爲以下數組:

[".td1",",","div"," ","*",",","body"]

而後咱們就能夠根據這個符號流進行工做。因爲沒有指定上下文對象,就從document開始,發現第一個是類選擇器,能夠用getElementsByClassName,若是沒有原生的,咱們仿照一個也不是難事。而後是並聯選擇器,將上面獲得的結果放進結果集。接着是標籤選擇器,使用getElementsByTgaName。接着是後代選擇器,這裏能夠優化,咱們能夠預先查看一個選擇器羣組是什麼,發現是通配符選擇器,所以繼續使用getElementsByTgaName。接着又是並聯選擇器,將上面結果放入結果集。最後一個是標籤選擇器,又使用getElementsByTgaName。最後是去重排序

顯然,有切割好的符號,工做簡單多了。

但沒有東西一開始就是完美的,好比咱們遇到一個這樣的選擇符,"nth-child(2n+1)".這是一個單獨的子元素過濾僞類,它不該該在這裏被分析。後面有專門的正則對它的僞類名與傳參進行處理。在切割器裏,它能獲得最小的詞素是選擇器!

因而切割器進行改進

    //讓小括號裏邊的東西不被切割
    var reg = /[\w\u00a1-\uFFFF][\w\u00a1-\uFFFF-]*|[#.:\[](?:[\w\u00a1-\uFFFF-]|\([^\)]*\)|\])+|(?:\s*)[>+~,*](?:\s*)|\s+/g

咱們不斷增長測試樣例,咱們問他愈來愈多,如這個選擇符 :「.td1[aa='>111']」 ,在這種狀況下,屬性選擇器被拆碎了!

[".td1","[aa",">","111"]

因而正則改進以下:

    //確保屬性選擇器做爲一個完整的詞素
    var reg = /[\w\u00a1-\uFFFF][\w\u00a1-\uFFFF-]*|[#.:](?:[\w\u00a1-\uFFFF-]|\S*\([^\)]*\))+|\[[^\]]*\]|(?:\s*)[>+~,*](?:\s*)|\s+/g

對於選擇符"td + div span",若是最後有一大堆空白,會致使解析錯誤,咱們確保後代選擇器夾在兩個選擇器之間

["td", "+", "div", " ", "span", " "]

最後一個選擇器會被咱們的引擎認做是後代選擇器,須要提早去掉

    //縮小後迭代選擇器的範圍
    var reg = /[\w\u00a1-\uFFFF][\w\u00a1-\uFFFF-]*|[#.:](?:[\w\u00a1-\uFFFF-]|\S+\([^\)]*\))+|\[[^\]]*\]|(?:\s*)[>+~,*](?:\s)|\s(?=[\w\u00a1-\uFFFF*#.[:])/g

若是咱們也想將前面的空白去掉,可能不是一個單獨的正則能作到的。如今切割器已經被咱們搞的至關複雜了。維護性不好。在mootools中等引擎中,裏邊的正則表達式更加複雜,多是用工具生成的 。到了這個地方,咱們須要轉換思路,將切割器該爲一個函數處理。固然,它裏邊也少了很多正則。正則是處理字符串的利器。

    var reg_split = /^[\w\u00a1-\uFFFF\-\*]+|[#.:][\w\u00a1-\uFFFF-]+(?:\([^\])*\))?|\[[^\]]*\])|(?:\s*)[>+~,](?:\s*)|\s(?=[\w\u00a1-\uFFFF*#.[:])|^\s+/;
    
    var slim = /\s+|\s*[>+~,*]\s*$/

    function spliter(expr) {
        var flag_break = false;
        var full = []; //這裏放置切割單個選擇器羣組獲得的詞素,以,爲界
        var parts = []; //這裏放置切割單個選擇器組獲得的詞素,以關係選擇器爲界
        do {
            expr = expr.replace(reg_split,function(part) {
                if (part === ",") { //這個切割器只處理到一個並聯選擇器
                    flag_break = true;
                } else {
                    if (part.match(slim)) { //對於關係並聯。通配符選擇器兩邊的空白進行處理
                        //對parts進行反轉,例如 div.aaa,反轉先處理aaa
                        full = full.concat(parts.reverse(),part.replace(/\s/g, ''));
                        parts = [];
                    } else {
                        parts[parts.length] = part
                    }
                }
                return "";//去掉已經處理了的部分
            });
            if (flag_break) 
                break;
        } while (expr)
            full =full.concat(parts.reverse());
            !full[0] && full.shift(); //去掉開頭的第一個空白
            return full;
    } 
    var expr = " div >  div#aaa,span"
    console.log(spliter(expr)); //=>  ["div", ">", "div"]

固然,這個相對於sizzle1.8與slick等引擎來講,不值一提,須要有很是深厚的正則表達式功力,深層的知識寶庫自動機理論才能寫出來

5.屬性選擇器對於空白字符的匹配策略

上文已經介紹過屬性選擇器的七種形態了,但屬性選擇器並無這麼簡單,在w3c草案對屬性選擇器[att~=val]提到了一個點,val不能爲空白字符,不然比較值flag(flag爲val與元素實際值比較結果)返回false。若是querySlelectorAll測試一下屬性其餘狀態,咱們會獲得更多相似結果。

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta charset="utf-8">
</head>
<body>
<script type="text/javascript">
window.onload = function () {
    console.log(document.querySelector("#test1[title='']")); //<div title="" id="test1"></div>
    console.log(document.querySelector("#test1[title~='']")); // null
    console.log(document.querySelector("#test1[title|='']")); //<div title="" id="test1"></div>
    console.log(document.querySelector("#test1[title^='']")); //null
    console.log(document.querySelector("#test1[title$='']")); // null
    console.log(document.querySelector("#test1[title*='']")); //null
    console.log("==========================================")
    console.log(document.querySelector("#test2[title='']")); //null
    console.log(document.querySelector("#test2[title~='']")); //null
    console.log(document.querySelector("#test2[title|='']")); //null
    console.log(document.querySelector("#test2[title^='']")); //null
    console.log(document.querySelector("#test2[title$='']")); //null
    console.log(document.querySelector("#test2[title*='']")); //null
}
</script>

<div title="" id="test1"></div>
<div title="aaa" id="test2"></div>
</body>
</html>

換言之,只要val爲空,除=或|=除外,flag必爲false,而且非=,!=操做符,若是取得值爲空白字符,flag也必爲false.

6.子元素過濾僞類的分級與匹配

子元素過濾僞類是css3新增的一種選擇器。比較複雜,這裏單獨放出來講。首先,咱們要將它從選擇符中分離出來。這個通常由切割器搞定。而後咱們用正則將僞類名與它小括號裏的傳參分解出來

以下是Icarus的作法

    var expr = ":nth-child(2n+1)"
    var rsequence = /^([#\.:])|\[\s*]((?:[-\w]|[^\x00-\xa0]|\\.)+)/
    var rpseudo = /^\(\s*("([^"]*)"|'([^']*)'|[^\(\)]*(\([^\(\)]*\))?)\s*\)/
    var rBackslash = /\\/g
        //這裏把僞類從選擇符裏分散出來
        match = expr.match(rsequence); //[":nth-child",":",":nth-child"]
        expr = RegExp.rightContext; //用它左邊的部分重寫expr--> "(2n+1)"
           key = (match[2] || "").replace(rBackslash, ""); //去掉換行符 key=--> "nth-child" 
    switch (match[1]) {
        case "#":
             //id選擇器 略
             break;
        case ".":
             //類選擇器 略
             break;
        case ":":
             //僞類 略
             tmp = Icarus.pseudoHooks[key];
             //Icarus.pseudoHooks裏邊放置咱們所能處理的僞類
             expr = RegExp.rightContext;//繼續取它左邊的部分重寫expr
             if ( !! ~key.indexOf("nth")) { //若是子元素過濾僞類
                 args = parseNth[match[1]] || parseNth(match[1]);//分解小括號的傳參
             } else {
                 args = match[3] || match [2] || match[1]
             }
        
        break;
        default:
            //屬性選擇器 略
            break;
    }

這裏有個小技巧,咱們須要不斷把處理過的部分從選擇器中去掉。通常選擇器引擎是使用expr = expr.replace(reg,"")進行處理,Icarus巧妙的使用正則的RegExp.rightContext進行復寫,將小括號裏邊的字符串取得咱們經過parseNTH進行加工。將數字1,4,單詞even,odd,-n+1等各類形態轉換爲an+b的形態

    function parseNth (expr) {
        var orig = expr
        expr = expr.replace(/^\+|\s*/g, '');//清除掉無用的空白
        var match = (expr === "even" && "2n" || expr === "odd" && "2n+1" || !/\D/.test(expr) && "0n+" + expr || expr).match(/(-?)(\d*)n([-+]?\d*)/);
        return parse_nth[orig] = {
            a: (match[1] + (match[2] || 1) - 0 ,
            b: match[3] - 0)
        };
    }

parseNth是一個緩存函數,這樣能避免重複解析,提升引擎的整體性能(緩存的精髓)

關於緩存的利用,能夠參看Icarus Sizzle1.8+ Slice等引擎,需求本身可尋找。

 5.sizzle引擎

jQuery最大的特色就是其選擇器,jQuery1.3開始裝其Sizzle引擎。sizzle引擎與當時主流的引擎大不同,人們說它是從右至左選擇(雖然不對,但大體方向如此),速度遠勝當時選擇器(不過當時也沒有什麼選擇器,所以sizzle一直自娛自樂)

Sizzle當時有如下幾個特色

i容許關係選擇器開頭
ii容許反選擇器套取反選擇器
iii大量的自定義僞類,好比位置僞類(eq,:first:even....),內容僞類(:contains),包含僞類(:has),標籤僞類(:radio,:input,:text,:file...),可見性僞類(:hidden,:visible)
iiii對結果進行去重,以元素在DOM樹的位置進行排序,這樣與將來出現的querySelector行爲一致。

 顯然,搞出這麼東西,不是一朝半夕的事情,說明john Resig研發了好久。當時sizzle的版本號爲0.9.1,代碼風格跟jQuery大不同,很是整齊清晰。這個風格一直延續到jQuery.1.7.2,Sizzle版本也跟上爲1.7.2,在jQuery.1.8時,或sizzle1.8時,風格大變,首先裏邊的正則式是經過編譯獲得的,以求更加準確,結構也異常複雜,開始走ext那樣編譯函數的路子,經過多種緩存手段提升查詢速度和匹配速度

因爲第二階段的Sizzle蛻變尚未完成,每星期都在變,tokenize ,addMatcher,matcherFrom,Tokens,matcherFormGroupMachers,complile這些關鍵的內部函數都在改進,不斷膨脹。咱們看是來看看sizzle1.72,這個版本是john Resing第一個階段的最完美的思想結晶

當時,Sizzle的總體結構以下:

i.Sizzle主函數,裏邊包含選擇符的切割,內部循環調用住查找函數,主過濾函數,最後是去重過濾。
ii.其它輔助函數,如uniqueSort, matches, matchesSelector
iii.Sizzle.find主查找函數
iiii.Sizzle.filiter過濾函數
iiiii.Sizzle.selectors包含各類匹配用的正則,過濾用的正則,分解用的正則,預處理用的函數,過濾函數等
iiiiii.根據瀏覽器特徵設計makeArray,sortOrder,contains等方法
iiiiiii.根據瀏覽器特徵重寫Sizzle.selectors中的部分查找函數,過濾函數,查找次序。
iiiiiiii.若瀏覽器支持querySelectorAll,那麼用它重寫Sizzle,將原來的sizzle做爲後備方案包裹在新的sizzle裏邊
iiiiiiiii.其它輔助函數,如isXML,posProcess

 下面使用一部分源碼分析下1.7.2sizzle

    var Sizzle = function(slelctor, context, results, seed) {
    //經過短路運算符,設置一些默認值
        results = results || [];
        context = context || document;
        //備份,由於context會被改寫,若是出現並聯選擇器,就沒法確保當前節點是對於哪個context

        var origContext = context;
    //上下文對象必須是元素節點或文檔對象
        if (context.nodeType !== 1 && context.nodeType !== 9) {
            return [];
        }
    //選擇符必須是字符,且不能爲空
        if (!slelctor || typeof slelctor !== "string") {
            return results;
        }    

        var m, set, checkSet, extra, ret, cur, pop, i,
            prune = true,
            contextXML = Sizzle.isXML(context),
            parts = [],
            soFar = slelctor;
    //下面是切割器的實現,每次只處理到並聯選擇器,extra給留下次遞歸自身時做傳參
    //不過與其餘引擎實現不一樣的是,它沒有一會兒切成選擇器,並且切成選擇器組與關係選擇器的集合
    //好比body div > div:not(.aaa),title
    //後代選擇器雖然被忽略了,但在循環這個數組時,它默認每兩個選擇器組與關係選擇器不存在就放在後代選擇器到那個位置上
        do {
            chunker.exec(""); //這一步主要講chunker的lastIndex重置,固然是直接設置chunker.lastIndex效果也同樣
            m = chunker.exec(soFar);
            if (m) {
                soFar = m[3];
                parts.push(m[1]);
                if (m[2]) { //若是存在並聯選擇器,就中斷
                    extra = m[3];
                    break;
                }
            }
        } while (m);
        // 略....
    }

接下來有許多分支,分別是對ID與位置僞類進行優化的(暫時跳過),着重幾個重要概念,查找函數,種子集,映射集。這裏集合sizzle源碼

查找函數就是Sizzle.selecters.find下的幾種函數,常規狀況下有ID,TAG,NAME三個,若是瀏覽器支持getElementByClassName,還會有Class函數。正如咱們前面所介紹的那樣,getElementById,geyElementsByName,getElementsByTagName,geyElementByClassName不能徹底信任他們,即使是標準瀏覽器都會有bug,所以四大查找函數都作了一層封裝,不支持返回undefined,其它則返回數組NodeList

種子集,或叫候選集,就是經過最右邊的選擇器組獲得的元素集合,好比說"div.aaa span.bbb",最右邊的選擇器組就是"span.bbb" ,這時引擎就會根據瀏覽器支持的狀況選擇getElemntByTagName或者getElementClassName獲得一組元素,而後經過className或tagName進行過濾。這時獲得的集合就是種子集,Sizzle的變量名seed就體現了這一點

映射集,或叫影子集Sizzle源碼的變量名爲checkSet。這是個怎樣的東西呢?當咱們取得種子集後,而是將種子集賦值一份出來,這就是映射集。種子集是由選擇器組選出來的,這時選擇符不爲空,必然往左就是關係選擇器。關係選擇器會讓引擎去選其兄長或父親(具體參見Sizzle.selector.relative下的四大函數),把這些元素置換到候選集對等的位置上。而後到下一個選擇器組時,就是純過濾操做。主過濾函數sizzle.filter會調用sizzle.seletors下N個過濾函數對這些元素進行監測,將不符合的元素替換爲false.所以,到最後去重排序時,映射集是一個包含布爾值與元素節點的數組(true也是在這個步奏中產生的)

種子集是分兩步選擇出來的 首先,經過Sizzle.find獲得一個大體的結果。而後經過Sizzle.filter,傳入最右那個選擇器組剩餘的部分作參數,縮小範圍。

    //這是徵對最左邊的選擇器組存在ID作出的優化
    ret = Sizzle.find(parts.shift(), context, contextXML);
    context = ret.expr ? Sizzle.filter(ret.expr, ret.set)[0] : ret.set[0]

    ret = seed ? {
        expr : parts.pop(),
        set : makeArray(seed)
        //這裏會對~,+進行優化,直接取它的上一級作上下文
    } : Sizzle.find(parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+" ) && context.parentNode ? context.parentNode : context, contextXML);

    set = ret.expr ? Sizzle.filter(ret.expr, ret.set) : ret.set;

咱們是先取span仍是取.aaa呢?這裏有個準則,確保咱們後面的映射集最小化。直白的說,映射集裏邊的元素越少,那麼調用的過濾函數的次數就越少,說明進入另外一個函數做用域所形成的耗時就越少,從而總體提升引擎的選擇速度。

爲了達到此目的,這裏作了一個優化,原生選擇器的調用順序被放到了一個叫Sizzle.selector.order的數組中,對於陳舊的瀏覽器,其順序爲ID,NANME,TAG,對於支持getElementByClassName的瀏覽器,其順序爲ID,Class,NAME,TAG。由於,ID至多返回一個元素節點,ClassName與樣式息息相關,不是每一個元素都有這個類名,name屬性帶來的限制可能比className更大,但用到的概率較少,而tagName可排除的元素則更少了。

那麼Sizzle.find就會根據上面的數組,取得它的名字,依次調用Sizzle.leftMatch對應的正則,從最右的選擇器組切割下須要的部分,將換行符處理掉,經過四大查找函數獲得一個粗糙的節點集合。若是獲得"[href=aaa]:visible"這樣的選擇符,那麼只有把文檔中全部節點做爲結果返回。

//sizzle.find爲主查找函數
    Sizzle.find = function(expr, context, isXML) {
        var set, i, len, match, type, left;

        if (!expr) {
            return [];
        }

        for (i = 0, len = Expr.order.length; i < len; i++) {
            type = Expr.order[i];
            //讀取正則,匹配想要的id class name tag
            if ((match = Expr.leftMatch[type].exec(expr))) {
                left = match[1];
                match.splice(1, 1);
                //處理換行符
                if (left.substr(left.length - 1) !== "\\") {
                    match[1] = (match[1] || "").replace(rBackslash, "");
                    set = Expr.find[type] (match, context, isXML);
                    //若是不爲undefined , 那麼取得選擇器組中用過的部分
                    if (set != null) {
                        expr = expr.replace(Expr.match[type], "");
                        break;
                    }
                }
            }
        }
        if (!set) { //沒有的話,尋找該上下文對象的全部子孫
            set = typeof context.getElementsByTagName !== "undefined" ? context.getElementsByTagName("*") : [];
        }

        return {
            set: set,
            expr : expr
        };
    };

通過主查找函數處理後,咱們獲得一個初步的結果,這時最右邊的選擇器可能還有殘餘,好比「div span.aaa」可能餘下"div span","div .aaa.bbb"可能餘下「div .bbb」,這個轉交主過濾函數Sizzle.filter函數處理

它有兩種不一樣的功能,一是不斷的縮小集合的個數,構成種子集返回。另外一種是將原集合中不匹配的元素置換爲false。這個根據它的第三個傳參inplace而定。

    Sizzle.filter = function (expr, set, inplace, not) {
        //用於生成種子集或映射集,視第三個參數而定
        //expr: 選擇符
        //set: 元素數組
        //inplace: undefined, null時進入種子集模式,true時進入映射集模式
        //not: 一個布爾值,來源自去反選擇器
        .....
    }

 待咱們把最右邊的選擇器組的最後都去掉後,種子集宣告完成,而後處理下一個選擇器組,並將種子集複製一下,生成映射集。在關係選擇器4個對應函數———他們爲Sizzle.selectors.relative命名空間下————只是將映射集裏邊的元素置換爲它們的兄長父親,個數是不變。所以映射集與種子集的數量老是至關。另外,這四個函數內部也在調用Sizzle.filter函數,它們的inplace參數爲true,走映射集的邏輯。

若是存在並聯選擇器,那就再調用Sizzle主函數,把獲得兩個結果合併去重

if (extara) {
    Sizzle(extra, origContext, results, seed);
    Sizzle.uniqueSort(result)
}


這個過程就是Sizzle的主流程。下面將是根據瀏覽器的特性優化或調整的部分好比ie6 7下的getElementById有bug,須要沖洗Expr.find.ID與Expr.filterID.ie6-ie8下,Array.prototype.slice.call沒法切割NodeList,須要從寫makeArray.IE6-8下,getElementsByTagName("*")會混雜在註釋節點,須要從寫Expr.find.TAG,若是瀏覽器支持querySelectorAll,那麼須要重寫整個Sizzle.

下面就從寫個瀏覽器支持querySelectorAll的方法把:

    if (document.querySelectorAll) { //若是支持querySelectorAll
        (function(){
            var oldSizzle = Sizzle,
                div = document.createElement("div"),
                id = "__sizzle__";
            div.innerHTML = "<p class='TEST'>test</p>";
            //safari在怪異模式下querySelectorAll不能工做,終止從寫
            if (div.querySelectorAll && div.querySelectorAll(".TEST").length === 0) {
                return;
            }

            Sizzle = function(query, context, extra, seed) {
                context = context || document;
                //querySelectorAll只能用於HTML文檔,在標準瀏覽器XML文檔中實現了接口,但不工做
                if (!seed && !Sizzle.isXML(context)) {
                    //See if we find a selector to speed up
                    var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(query);

                    if (match && (context.nodeType === 1 || context.nodeType === 9)) { //元素Element文檔Document
                        //優化只有單個標籤選擇器的狀況
                        if (match[1]) {
                            return makeArray(context.getElementsByTagName(query), extra);
                            //優化只有單個類選擇的狀況
                        } else if (match[2] && Expr.find.CLASS && context.getElementsByClassName) {
                            return makeArray(context.getElementsByClassName(match[2]), extra);
                        }
                    }

                    if (context.nodeType === 9) { //文檔Document
                        //優化選擇符爲body的狀況
                        //由於文檔只有它一個標籤,而且對於屬性直接取它
                        if (query === "body" && context.body) {
                            return makeArray ([context.body], extra);
                            //優化只有ID選擇器的狀況
                            //speed-up: Sizzle("ID")
                        } else if (match && match[3]) {
                            var elem = context.getElementById(match[3]);
                            //注意,瀏覽器也會優化,它會緩存了上次的結果
                            //即使他如今移除了DOM樹
                            if (elem && elem.parentNode) {
                                //ie和opera會混淆id和name,確保id等於目標值
                                if (elem.id === match[3]) {
                                    return makeArray([elem], extra);
                                }
                            } else {
                                return makeArray([], extra);
                            }
                        }
                        try {
                            return makeArray(context.querySelectorAll(query), extra);
                        } catch (queryError) {}
                        //ie8下的querySelectorAll實現存在bug,它會包含本身的集合內查找符合本身的元素節點
                        //根據規範,應該是在當前上下文中的全部子孫元素下查找,IE8下若是元素節點爲Object,沒法查找元素
                    } else if (context.nodeType === 1 && context.nodeName.toLowerCase() !== "object") {
                        var oldContext = context;
                            old = context.getElementById("id"),
                            nid = old || id,
                            hasParent = context.parentNode,
                            relativeHierarchySelector = /^\s*[+~]/.test(query);
                            if (!old) {
                                context.setAttribute("id" , nid);
                            } else {
                                nid = nid.replace(/'/g, "\\$&");
                            }
                            if (relativeHierarchySelector && hasParent) {
                                context = context.parentNode;
                            }
                    // 若是存在id ,則將id取出來放到這個分組的最前面,好比div b --> [id=xxx] div b
                    //不存在id,就建立一個,重複上面的操做,最後會刪掉這個id
                            try {
                                if (!relativeHierarchySelector || hasParent) {
                                    return makeArray(context.querySelectorAll("[id='" + nid +"']" + query), extra);
                                }
                            } catch (pseudoError) {} finally {
                                if (!old) {
                                    oldContext.removeAttribute("id");
                                }
                            }
                    }
                }

                return oldSizzle(query, context, extra, seed);
            };
            //將原來的方法從新綁定到Sizzle函數上
            for (var prop in oldSizzle) {
                Sizzle[prop] = oldSizzle[prop];
            }

            //release memory in IE
            div = null
        })();
    }

 從源碼中能夠看出,它不僅僅是重寫那麼簡單,根據不一樣的狀況還有各類提速方案。getElementById自不用說,速度確定快,這內部作了緩存,並且getElementById最多隻返回一個元素節點,而querySelectorAll則會返回擁有這個ID值的多個元素。這個聽起來有點奇怪,querySelectorAll不會理會你的錯誤行爲,機械執行指令。

另外getElementsByTagName也是內部使用了緩存,它也比querySelectorAll快getElementsByTagName返回的是一個NodeList對象,而querySelectorAll返回的是一個StaticNodeList對象。一個是動態的,一個是靜態的

測試下不一樣:

    var tag = "getElementsByTagName", sqa = "querySelectorAll";
    console.log(document[tag]("div") == document[tag]("div")); //=>true
    console.log(document[sqa]("div") == document[sqa]("div")); //=>false

ps:true意味着它們拿到的同是cache引用,Static每次返回都是不同的Object

往上有數據代表,建立一個動態的NodeList對象比一個靜態的StaticNodeList對象快90%

querySelectorAll的問題遠不至與此,IE8中微軟搶先實現了它,那時徵對它的規範尚未完成,所以不明確的地方微軟自行發揮了。IE8下若是對StaticNodeList取下標超界,不會返回undefined,而是拋出異常 Invalid  procedure call or argument。

    var els = document.body.querySelectorAll('div');
    alert(els[2])//2> els.length-1

所以,一些奇特的循環,要適可而止。

最後,說明下querySelectorAll這個API在不一樣的瀏覽器。不一樣的版本中都存在bug,不支持的選擇器類型多了,咱們須要在工做中作充分的測試(Sizzle1.9中,中槍的僞類就有focus , :enabled , :disabled , :checked.....)。


在現實工做中,想要支持選擇器類型越多,就須要在結構上設計的有擴展性。但過度添加直接的定義僞類,就意味着將來與querySelectorAll發生的衝突越多。像zopto.js,就是一個querySelectorAll當成本身選擇器的引擎,Sizzle1.9時已經有1700行代碼。最近jQuery也作了一個selector-native模塊,審視將來。

 

本文完結,歡迎關注下章內容

 

上一章:第六章 第六章:類工廠  下一章 :第八章:節點模塊

相關文章
相關標籤/搜索