日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

6.选择器引擎

發布時間:2023/12/20 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 6.选择器引擎 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

jQuery憑借選擇器風靡全球,各大框架類庫都爭先開發自己的選擇,一時間內選擇器變為框架的標配

早期的JQuery選擇器和我們現在看到的遠不一樣。最初它使用混雜的xpath語法的selector。
第二代轉換為純css的自定義偽類,(比如從xpath借鑒過來的位置偽類)的sizzle,但sizzle也一直在變,因為他的選擇器一直存在問題,一直到JQuery1.9才搞定,并最終全面支持css3的結構偽類。

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

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

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

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

有資料可查的是 getElementById , getElementByTagName是ie5引入的。那是1999年的事情,伴隨一個輝煌的產品,window98,捆綁在一起,因此,那時候ie都傾向于為IE做兼容。

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

但人們很快發現問并無法選取題了,就是IE的getElementById是不區分表單元素的ID和name,如果一個表單元素只定義name并與我們的目標元素同名,且我們的目標元素在它的后面,那么就會選錯元素,這個問題一直延續到ie7.

IE下的getElementsByTagesName也有問題。當參數為*號通配符時,它會混入注釋節點,并無法選取Object下的元素。

(解決辦法略去)

此外,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){//取得一個元素的子孫,并兼容ie5return 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;}//如果沒有 # . [ 這樣的特殊字符,我們就當是tagNamevar 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,我們查找元素就非常方便。如下表

查找元素
?遍歷所有子節點遍歷所有子元素
第一個firstChildfirstElementChild
最后一個lastChildlastElementChild
前面的previousSiblingpreviousElementSibling
后面的nextSiblingnextElementSibling
父節點parentNodeparentElement
數量  lengthchildElementCount

偽類

偽類是選擇器家族中最龐大的家族,從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 iddentifiderif (!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.15if (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??
0000000元素一致
0000011節點在不同的文檔(或者一個在文檔之外)
0000102節點B在節點A之前
0001004節點A在節點B之前
0010008節點B包含節點A
01000016節點A包含節點B
10000032瀏覽器的私有使用

有時候,兩個元素的位置關系可能連續滿足上表的兩者情況,比如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:比較那些邊界點:為常數

  • Range.START_TO_START 比較兩個Range節點的開始點
  • Range.END_TO_END 比較兩個Range節點的結束點
  • Range.START_TO_END 用sourceRange的開始點與當前范圍的結束點比較
  • 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//不斷往上取,一值取到HTMLwhile (ap && ap.nodeType === 1) {al[al.length] = apap = ap[p]}var bl =[],bp = b;while (bp && bp.nodeType === 1) {bl[bl.length] = bpbp = 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,反轉先處理aaafull = 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~='']")); // nullconsole.log(document.querySelector("#test1[title|='']")); //<div title="" id="test1"></div>console.log(document.querySelector("#test1[title^='']")); //nullconsole.log(document.querySelector("#test1[title$='']")); // nullconsole.log(document.querySelector("#test1[title*='']")); //nullconsole.log("==========================================")console.log(document.querySelector("#test2[title='']")); //nullconsole.log(document.querySelector("#test2[title~='']")); //nullconsole.log(document.querySelector("#test2[title|='']")); //nullconsole.log(document.querySelector("#test2[title^='']")); //nullconsole.log(document.querySelector("#test2[title$='']")); //nullconsole.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;//繼續取它左邊的部分重寫exprif ( !! ~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 = exprexpr = 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會被改寫,如果出現并聯選擇器,就無法確保當前節點是對于哪一個contextvar 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 tagif ((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 upvar 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,就創建一個,重復上面的操作,最后會刪掉這個idtry {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 IEdiv = 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")); //=>trueconsole.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模塊,審視未來。

    轉載于:https://www.cnblogs.com/wingzw/p/7360236.html

    總結

    以上是生活随笔為你收集整理的6.选择器引擎的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 就去吻综合| 中文字幕在线视频免费播放 | 性人久久久久 | www啪啪| 中文字幕人乱码中文字 | 在线观看中文字幕一区二区 | 在线免费黄网 | 99热这里只有精品3 成年人黄色网址 | 亚洲天堂第一区 | 国产精品传媒麻豆hd | 欧美日本一道本 | 国产精品久久久久久久成人午夜 | 91涩漫成人官网入口 | 黑花全肉高h湿play短篇 | av男人的天堂网 | 夜夜骚av一区二区三区 | 久久久99精品免费观看 | 欧美视频久久久 | 国产一线二线三线女 | 婷婷玖玖 | 国产aⅴ精品一区二区果冻 台湾性生生活1 | 美女日批视频在线观看 | 国产美女免费网站 | 91精品人妻一区二区三区蜜桃欧美 | 国产视频h| 五月婷婷亚洲 | av网站免费在线看 | 丰满少妇高潮久久三区 | 婷婷影音 | 五月天一区二区三区 | 欧洲mv日韩mv国产 | 亚洲aa | 99精品视频网站 | 日本a级一区 | 国内视频一区二区三区 | 一级国产黄色片 | 亚洲天堂免费在线观看视频 | 黄色在线视频网站 | 少妇被又大又粗又爽毛片久久黑人 | 日韩av电影网址 | 精品人伦一区二区三区蜜桃免费 | 国产精品3 | 国产一二三区在线视频 | 国产裸体无遮挡 | 牛牛视频在线观看 | 欧美亚洲图片小说 | 国产a v一区二区三区 | 热久久中文字幕 | 国产成人短视频 | 在线高清观看免费观看 | 老狼影院伦理片 | 中文字幕无码日韩专区免费 | 麻豆91精品 | 他趴在我两腿中间添得好爽在线看 | 中文字幕在线观看亚洲 | 视频在线观看一区 | 国产suv精品一区 | 国产成人无遮挡在线视频 | 99国产精品一区二区三区 | 国精产品一品二品国精品69xx | 精品一区二区三区在线播放 | 国产午夜免费视频 | 性色av无码久久一区二区三区 | 精品亚洲aⅴ无码一区二区三区 | 久青草免费视频 | 国产精久久 | 一级黄色在线 | av一级黄色 | 青青操在线 | 五月激情av | 亚洲欧美日韩精品在线观看 | 可以免费在线观看的av | 在线色播 | 久久国产毛片 | 理论片琪琪午夜电影 | 邻居校草天天肉我h1v1 | 免费在线国产精品 | 久久99热人妻偷产国产 | 久久新网址 | 男插女视频免费 | 国产99久久 | 成人tv| 波多野结衣喷潮 | 一区二区精品视频 | 波多野结衣先锋影音 | 无毒黄色网址 | 国产日产欧美一区二区三区 | 国产精品久久久爽爽爽麻豆色哟哟 | 亚洲aaaaaaa| 国产第一页在线播放 | 欧美激情校园春色 | 在线看中文字幕 | 粉色午夜视频 | 好吊日在线观看 | 午夜免费高清视频 | 日韩欧美在线一区二区三区 | 国产一卡二卡在线播放 | www久久| 原创少妇半推半就88av |