6.选择器引擎
jQuery憑借選擇器風(fēng)靡全球,各大框架類庫都爭(zhēng)先開發(fā)自己的選擇,一時(shí)間內(nèi)選擇器變?yōu)榭蚣艿臉?biāo)配
早期的JQuery選擇器和我們現(xiàn)在看到的遠(yuǎn)不一樣。最初它使用混雜的xpath語法的selector。
第二代轉(zhuǎn)換為純css的自定義偽類,(比如從xpath借鑒過來的位置偽類)的sizzle,但sizzle也一直在變,因?yàn)樗倪x擇器一直存在問題,一直到JQuery1.9才搞定,并最終全面支持css3的結(jié)構(gòu)偽類。
2005 年,Ben Nolan的Behaviours.js 內(nèi)置了聞名于世的getElementBySelector,是第一個(gè)集成事件處理,css風(fēng)格的選擇器引擎與onload處理的類庫,此外,日后的霸主prototype.js頁再2005年誕生。但它勉強(qiáng)稱的上是,選擇器$與getElementByClassName在1.2出現(xiàn),事件處理在1.3,因此,Behaviour.js還風(fēng)光一時(shí)。
本章從頭至尾實(shí)驗(yàn)制造一個(gè)選擇器引擎。再次,我們先看看前人的努力:
1.瀏覽器內(nèi)置尋找元素的方法
請(qǐng)不要追問05年之前開發(fā)人員是怎么在這種缺東缺西的環(huán)境下干活的。那時(shí)瀏覽器大戰(zhàn)正酣。程序員發(fā)明navugator.userAgent檢測(cè)進(jìn)行"自保"!網(wǎng)景戰(zhàn)敗,因此有關(guān)它的記錄不多。但I(xiàn)E確實(shí)留下不少資料,比如取得元素,我們直接可以根據(jù)id取得元素自身(現(xiàn)在所有瀏覽器都支持這個(gè)特性),不通過任何API ,自動(dòng)映射全局變量,在不關(guān)注全局污染時(shí),這是個(gè)很酷的特性。又如。取得所有元素,使用document.All,取得某一種元素的,只需做下分類,如p標(biāo)簽,document.all.tags("p")。
有資料可查的是 getElementById , getElementByTagName是ie5引入的。那是1999年的事情,伴隨一個(gè)輝煌的產(chǎn)品,window98,捆綁在一起,因此,那時(shí)候ie都傾向于為IE做兼容。
(感興趣的話參見讓ie4支持getElementById的代碼,此外,還有g(shù)etElementByTagsName的實(shí)現(xiàn))
但人們很快發(fā)現(xiàn)問并無法選取題了,就是IE的getElementById是不區(qū)分表單元素的ID和name,如果一個(gè)表單元素只定義name并與我們的目標(biāo)元素同名,且我們的目標(biāo)元素在它的后面,那么就會(huì)選錯(cuò)元素,這個(gè)問題一直延續(xù)到ie7.
IE下的getElementsByTagesName也有問題。當(dāng)參數(shù)為*號(hào)通配符時(shí),它會(huì)混入注釋節(jié)點(diǎn),并無法選取Object下的元素。
(解決辦法略去)
此外,w3c還提供了一個(gè)getElementByName的方法,這個(gè)IE也有問題,它只能選取表單元素。
在Prototype.js還未到來之前,所有可用的只有原生選擇器。因此,simon willson高出getElementBySelector,讓世人眼前一亮。
之后的過程就是N個(gè)版本的getElementBySlelector,不過大多數(shù)是在simon的基礎(chǔ)上改進(jìn)的,甚至還討論將它標(biāo)準(zhǔn)化!
getElementBySlelector代表的是歷史的前進(jìn)。JQuery在此時(shí)優(yōu)點(diǎn)偏向了,prototype.js則在Ajax熱浪中扶搖直上。不過,JQuery還是勝利了,sizzle的設(shè)計(jì)很特別,各種優(yōu)化別出心裁。
Netscape借助firefox還魂,在html引入xml的xpath,其API為document.evaluate.加之很多的版本及語法復(fù)雜,因此沒有普及開來。
微軟為保住ie占有率,在ie8上加入querySelector與querySlectorAll,相當(dāng)于getElementBySelector的升級(jí)版,它還支持前所未有的偽類,狀態(tài)偽類。語言偽類和取反偽類。此時(shí),chrome參戰(zhàn),激發(fā)瀏覽器標(biāo)準(zhǔn)的熱情和升級(jí),ie8加入的選擇器大家都支持了,還支持的更加標(biāo)準(zhǔn)。此時(shí),還出現(xiàn)了一種類似選擇器的匹配器————matchSelector,它對(duì)我們編寫選擇器引擎特別有幫助,由于是版本號(hào)競(jìng)賽時(shí)誕生的,誰也不能保證自己被w3c采納,都帶有私有前綴。現(xiàn)在css方面的Selector4正在起草中,querySeletorAll也只支持到selector3部分,但其間兼容性問題已經(jīng)很雜亂了。
2.getElementsBySelector
讓我們先看一下最古老的選擇器引擎。它規(guī)定了許多選擇器發(fā)展的方向。在解讀中能涉及到很多概念,但不要緊,后面有更詳細(xì)的解釋。現(xiàn)在只是初步了解下大概藍(lán)圖。
/* document.getElementsBySelector(selector)version 0.4 simon willson march 25th 2003-- work in phonix0.5 mozilla1.3 opera7 ie6 */function getAllchildren(e){//取得一個(gè)元素的子孫,并兼容ie5return e.all ? e.all : e.getElementsByTgaName('*');}document.getElementsBySelector = function(selector){//如果不支持getElementsByTagName 則直接返回空數(shù)組if (!document.getElementsByTgaName) {return new Array();}//切割CSS選擇符,分解一個(gè)個(gè)單元格(每個(gè)單元可能代表一個(gè)或多個(gè)選擇器,比如p.aaa則由標(biāo)簽選擇器和類選擇器組成)var tokens = selector.split(' ');var currentContext = new Array(document);//從左至右檢測(cè)每個(gè)單元,換言此引擎是自頂向下選擇元素//如果集合中間為空,立即中至此循環(huán)for (var i = 0 ; i < tokens.length; i++) {//去掉兩邊的空白(并不是所有的空白都沒有用,兩個(gè)選擇器組之間的空白代表著后代迭代器,這要看作者們的各顯神通)token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');//如果包含ID選擇器,這里略顯粗糙,因?yàn)樗赡茉谝?hào)里邊。此選擇器支持到屬性選擇器,則代表著可能是屬性值的一部分。if (token.indexOf('#') > -1) {//假設(shè)這個(gè)選擇器是以tag#id或#id的形式,可能導(dǎo)致bug(但這些暫且不談,沿著作者的思路看下去)var bits =token.split('#');var tagName = bits[0];var id = bits[1];//先用id值取得元素,然后判定元素的tagName是否等于上面的tagName//此處有一個(gè)不嚴(yán)謹(jǐn)?shù)牡胤?#xff0c;element可能為null,會(huì)引發(fā)異常var element = document.getElementById(id);if(tagName && element.nodeName.toLowerCase() != tagName) {//沒有直接返回空結(jié)合集return new Array();}//置換currentContext,跳至下一個(gè)選擇器組currentContext = new Array(element);continue;}//如果包含類選擇器,這里也假設(shè)它以.class或tag.class的形式if (token.indexOf('.') > -1){var bits = token.split('.');var tagName = bits[0];var className = bits[1];if (!tagName){tagName = '*';}//從多個(gè)父節(jié)點(diǎn),取得它們的所有子孫//這里的父節(jié)點(diǎn)即包含在currentContext的元素節(jié)點(diǎn)或文檔對(duì)象var found = new Array;//這里是過濾集合,通過檢測(cè)它們的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可能為空,因此不失為一種優(yōu)化手段,但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部分相似,其實(shí)應(yīng)該抽取成一個(gè)獨(dú)立的函數(shù)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;//根據(jù)第二個(gè)操作符生成檢測(cè)函數(shù),后面的章節(jié)有詳細(xì)介紹 ,請(qǐng)繼續(xù)關(guān)注哈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;}//如果沒有 # . [ 這樣的特殊字符,我們就當(dāng)是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; //返回最后的選集}?顯然當(dāng)時(shí)受網(wǎng)速限制,頁面不會(huì)很大,也不可能有很復(fù)雜的交互,因此javascript還沒有到大規(guī)模使用的階段,我們看到當(dāng)時(shí)的庫頁不怎么重視全局污染,也不支持并聯(lián)選擇器,要求每個(gè)選擇器組不能超過兩個(gè),否則報(bào)錯(cuò)。換言之,它們只對(duì)下面的形式CSS表達(dá)式有效:
#aa p.bbb [ccc=ddd]Css表達(dá)符將以空白分隔成多個(gè)選擇器組,每個(gè)選擇器不能超過兩種選取類型,并且其中之一為標(biāo)簽選擇器
要求比較嚴(yán)格,文檔也沒有說明,因此很糟糕。但對(duì)當(dāng)時(shí)編程環(huán)境來說,已經(jīng)是喜出望外了。作為早期的選擇器,它也沒有想以后那樣對(duì)結(jié)果集進(jìn)行去重,把元素逐個(gè)按照文檔出現(xiàn)的順序進(jìn)行排序,我們?cè)诘谝还?jié)指出的bug,頁沒有進(jìn)行規(guī)避,可能是受當(dāng)時(shí)javascript技術(shù)交流太少。這些都是我們要改進(jìn)的地方。
3.選擇器引擎涉及的知識(shí)點(diǎn)
本小節(jié)我們學(xué)習(xí)上小節(jié)的大力的概念,其中,有關(guān)選擇器引擎實(shí)現(xiàn)的概念大多數(shù)是從sizzle中抽取出來的,兒CSS表達(dá)符部分則是W3C提供的,首先從CSS表達(dá)符部分介紹。
h1 {color: red;font-size: 14px;}其中,h1 為選擇符,color和font-size為屬性,red和14px為值,兩組color: red和font-size: 14px;為它們的聲明。
上面的只是理想情況,重構(gòu)成員交給我們CSS文件,里邊的選擇符可是復(fù)雜多了。選擇符混雜著大量的標(biāo)記,可以分割為更細(xì)的單元。總的來說,分為四大類,十七種。此外,還包含選擇引擎無法操作的偽元素。
四大類:指并聯(lián)選擇器、 簡(jiǎn)單選擇器 、 關(guān)系選擇器 、 偽類
并聯(lián)選擇器:就是“,”,一種不是選擇器的選擇器,用于合并多個(gè)分組的結(jié)果
關(guān)系選擇器?分四種: 親子 后代 相鄰,通配符
偽類分為六種: 動(dòng)作偽類, 目標(biāo)偽類, 語言偽類, 狀態(tài)偽類, 結(jié)構(gòu)偽類, 取得反偽類。
簡(jiǎn)單的選擇器又稱為基本選擇器,這是在prototype.js之前的選擇器都已經(jīng)支持的選擇器類型。不過在css上,ie7才開始支持部分屬性選擇器。其中,它們?cè)O(shè)計(jì)的非常整齊劃一,我們可以通過它的一個(gè)字符決定它們的類型。比如id選擇器的第一個(gè)字符為#,類選擇器為. ,屬性選擇器為[ ,通配符選擇器為 * ;標(biāo)簽選擇器為英文字母。你可以可以解釋為什么沒有特殊符號(hào)。jQuery就是使用/isTag = !/\W/.test( part )進(jìn)行判定的。
在實(shí)現(xiàn)上,我們?cè)谶@里有很多原生的API可以使用,如getElementById. getElementsByTagName. getElementsByClassName. document.all?屬性選擇器可以用getAttribute 、 getAttributeNode attributes, hasAttribute,2003年曾經(jīng)討論引入getElementByAttribute,但沒成功,實(shí)際上,firefix上的XUI的同名就是當(dāng)時(shí)的產(chǎn)物。不過屬性選擇器的確比較復(fù)雜,歷史上他是分為兩步實(shí)現(xiàn)的。
css2.1中,屬性選擇器又以下四種狀態(tài)。
[att]:選取設(shè)置了att屬性的元素,不管設(shè)定值是什么。
[att=val]:選取了所有att屬性的值完全等于val的元素。
[att~=val]:表示一個(gè)元素?fù)碛袑傩詀tt,并且該屬性還有空格分割的一組值,其中之一為'val'。這個(gè)大家應(yīng)該能聯(lián)想到類名,如果瀏覽器不支持getElementsByClassName,在過濾階段,我們可以將.aaa轉(zhuǎn)換為[class~=aaa]來處理
[att|=val]:選取一個(gè)元素?fù)碛袑傩詀tt,并且該屬性含'val'或以'val-'開頭
Css3中,屬性選擇器又增加三種形態(tài):
[att^=val]:選取所有att屬性的值以val開頭的元素
[att$=val]:選取所有att屬性的值以val結(jié)尾的元素
[att*=val]:選取所有att屬性的值包含val字樣的元素。
以上三者,我們都可以通過indexOf輕松實(shí)現(xiàn)。
此外,大多選取器引擎,還實(shí)現(xiàn)了一種[att!=val]的自定義屬性選擇器。意思很簡(jiǎn)單,選取所有att屬性不等于val的元素,著正好與[att=val]相反。這個(gè)我們也可以通過css3的去反偽類實(shí)現(xiàn)。
我們?cè)倏纯?strong>關(guān)系選擇器。關(guān)系選擇器是不能單獨(dú)存在的,它必須在其他兩類選擇器組合使用,在CSS里,它必須夾在它們中間,但選擇器引擎可能允許放在開始。在很長(zhǎng)時(shí)間內(nèi),只存在后代選擇器(E F),就在兩個(gè)選擇器E與F之間的空白。css2.1又增加了兩個(gè),親子選擇器(E > F)與相鄰選取(E + F),它們也夾在兩個(gè)簡(jiǎn)單選擇器之間,但允許大于號(hào)或加號(hào)兩邊存在空白,這時(shí),空白就不是表示后代選擇器。CSS3又增加了一個(gè),兄長(zhǎng)選擇器(E ~ F),規(guī)則同上。CSS4又增加了一個(gè)父親選取器,不過其規(guī)則一直在變化。
后代選擇器:通常我們?cè)谝鎯?nèi)構(gòu)建一個(gè)getAll的函數(shù),要求傳入一個(gè)文檔對(duì)象或元素節(jié)點(diǎn)取得其子孫。這里要特別注意IE下的document.all,getElementByTagName ?的("*")混入注釋節(jié)點(diǎn)的問題。
親子選擇器:這個(gè)我們?nèi)绻淮蛩慵嫒軽ML,直接使用children就行。不過在IE5-8它都會(huì)混入注釋節(jié)點(diǎn)。下面是兼容列情況。
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;}相鄰選擇器: 就是取得當(dāng)前元素向右的一個(gè)元素節(jié)點(diǎn),視情況使用nextSibling或nextElementSibling.
function getNext (el) {if ("nextElementSibling" in el) {return el.nextElementSibling}while (el = el.nextSibling) {if (el.nodeType === 1) {return el;}}return null}兄長(zhǎng)選擇器:就是取其右邊的所有同級(jí)元素節(jié)點(diǎn)。
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規(guī)范的,用于遍歷元素節(jié)點(diǎn)。加上后來補(bǔ)充的parentElement,我們查找元素就非常方便。如下表
| ? | 遍歷所有子節(jié)點(diǎn) | 遍歷所有子元素 |
| 第一個(gè) | firstChild | firstElementChild |
| 最后一個(gè) | lastChild | lastElementChild |
| 前面的 | previousSibling | previousElementSibling |
| 后面的 | nextSibling | nextElementSibling |
| 父節(jié)點(diǎn) | parentNode | parentElement |
| 數(shù)量 | length | childElementCount |
偽類
偽類是選擇器家族中最龐大的家族,從css1開始,以字符串開頭,到css3時(shí)代,出現(xiàn)了要求傳參的機(jī)構(gòu)偽類和去反偽類。
(1).動(dòng)作偽類
動(dòng)作偽類又分為鏈接偽類和用戶行為偽類,其中,鏈接偽類由:visted和:link組成,用戶行為偽類分為:hover,:active, :focus組成。這這里我們基本上只能模擬:link,而在瀏覽器的原生的querySeletorAll對(duì)它們的支持也存在差異,ie8-ie10存在取:link錯(cuò)誤,它只能取a的標(biāo)簽,實(shí)際:link指代a aera link這三種標(biāo)簽,這個(gè)其它標(biāo)簽瀏覽器都正確。另外,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得到結(jié)果集合,因此,我們需要通過上一次得到的結(jié)果集就行過濾。在瀏覽器中,我們可以通過document.links得到部分結(jié)果,因此不包含link標(biāo)簽。因此,最好的方法是判定它的tagName是否等于A,LINK,AREA中的其中一個(gè)。
(2).目標(biāo)偽類
目標(biāo)偽類即:target偽類,指其id或者name屬性與url的hash部分(#之后的部分),匹配上的元素。
假如一個(gè)文檔,其id為section_2,而url中的hash部分也是#section_2,那么它就是我們要取的元素。
Sizzle中過濾的函數(shù)如下:
"target": function(elem) {var hash = window.location && window.location.hash;return hash && hash.slice(1) === elem.id;}(3).語言偽類
語言偽類即:length偽類,用來設(shè)置使用特殊語言的內(nèi)容樣式,如:lang(de)的內(nèi)部應(yīng)該為德語,需要特殊處理。
注意:lang 雖然為DOM元素的一個(gè)屬性,但:lang偽類與屬性選擇器有所不同,具體表現(xiàn):lang偽類具有“繼承性”,如下面的html表示的文檔
<html> <head> </head> <body lang="de"> <p>一個(gè)段落</p> </body> </html>如果使用[lang=de]則只能選擇到body元素,因?yàn)閜元素沒有l(wèi)ang屬性,但是使用:lang(de)則可以同時(shí)選擇到body和p元素,表現(xiàn)出繼承性。
"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).狀態(tài)偽類
狀態(tài)偽類用于標(biāo)記一個(gè)UI元素的當(dāng)前狀態(tài),有:checked , :enabled , :disable 和 :indeterminate這四個(gè)偽類組成。我們可以分別通過元素的checked , disabled , indeteminate屬性進(jìn)行判定。
?(5).結(jié)構(gòu)偽類
它又可以分為三種,根偽類,子元素過濾偽類,空偽類。
根偽類是由它在文檔的位置判定,子元素過濾偽類是根據(jù)它在其父親的所有孩子的位置或標(biāo)簽類型判定。空偽類是根據(jù)它孩子的個(gè)數(shù)判定。
:root偽類?用于選取根元素,在html文檔中,通常是html元素。
:nth-child?是所有子元素的過濾偽藍(lán)本,其它8種都是由它衍生出來的。它帶有參數(shù),可以是純數(shù)字,代數(shù)式或單詞,如果是數(shù)字,則從1計(jì)起,如果是代數(shù)式,n則從0遞增,非常不好理解的規(guī)則。
:nth-child(2)選取當(dāng)前父節(jié)點(diǎn)的第2個(gè)子元素
:nth-child(n+4)?選取大于等于4的的標(biāo)簽,我們可以將n看成自增量(0 <= n <= parent.children.length),此代數(shù)的值因變量。
:nth-child(-n+4)選取小于等于4標(biāo)簽
:nth-child(2n)選取偶數(shù)標(biāo)簽,2n也可以是even
:nth-child(2n-1)選取奇數(shù)標(biāo)簽,2n-1也可以是odd
:nth-child(3n+1)表示沒三個(gè)為一組,選取它的第一個(gè)
:nth-last-child與:nth-child差不多,不過是從后面取起來。比如:nth-last-child(2)
:nth-of-type和nth-last-of-type與:nth-child和nth-last-child類似,規(guī)則是將當(dāng)前元素的父節(jié)點(diǎn)的所有元素按照tagName分組,只要其參數(shù)符合它在那一組的位置就被匹配到。比如:nth-of-type(2),另外一例子,nth-of-type(even).
:frist-child用于選取第一個(gè)子元素,效果等同于:nth-child(1)
:last-child用于選取最后一個(gè)元素,效果等同于:nth-last-child(1)
:only-child用于選擇唯一的子元素,當(dāng)子元素的個(gè)數(shù)超過1時(shí),選擇器失效。
:only-of-type將父節(jié)點(diǎn)的元素按照tagName分組,如果某一組只有一個(gè)元素,那么就選擇這些元素返回。
:empty?用于選擇那些不包含任何元素的節(jié)點(diǎn),文本節(jié)點(diǎn),CDATA節(jié)點(diǎn)的元素,但允許里邊只存在注釋節(jié)點(diǎn)。
Sizzle中:empty的實(shí)現(xiàn)如下:
"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.實(shí)現(xiàn)如下。
"empty": function(node) {var child = node.firstChild;return !(child && child.nodeType == 1) && !(node.innerText || node.textContent || '').length;},(6).去反偽類
去反偽類即:not偽類,其參數(shù)為一個(gè)或多簡(jiǎn)單選擇器,里邊用逗號(hào)隔開。在jQuery等選擇器引擎中允許你傳入其它類型的選擇器,甚至可以進(jìn)行多個(gè)去反偽類嵌套。
(7).引擎實(shí)現(xiàn)時(shí)涉及的概念
種子集:或者叫候選集,如果css選擇符非常復(fù)雜,我們要分幾步才能得到我們想要的元素。那么第一次得到的元素集合就叫種子集。在Sizzle中,這樣基本從右到左,它的種子集中就有一部分為我們最后得到的元素。如果選擇器引擎是從左至右的選擇,那么它們只是我們繼續(xù)查找它的子元素或兄弟的“據(jù)點(diǎn)”而已。
結(jié)果集:選擇器引擎最終返回的元素集合們現(xiàn)在約定俗成,它要保持與querySlectorAll得到的結(jié)果一致,即,沒有重復(fù)元素。元素要按照他們?cè)贒OM樹上出現(xiàn)的順序排序。
過濾集:我們選取一組元素后,它之后每一個(gè)步驟要處理的元素集合都可以稱為過濾集。比如p.aaa,如果瀏覽器不支持querySelectorAll,若支持getElementByClassName,那么我們就用它得到種子集,然后通過className進(jìn)行過濾。顯然在大多數(shù)情況下,前者比后者快多了。同理,如果存在ID選擇器,由于ID在一個(gè)文檔中不重復(fù),因此,使用ID查找更快。在Sizzle下如果不支持QuerySlectorAll,它會(huì)只能地以ID,class,Tag順序去查找。
選擇器群組:一個(gè)選擇符被并聯(lián)選擇器","劃分成的每一個(gè)大組
選擇器組:一個(gè)選擇器群組被關(guān)系選擇器劃分為的第一個(gè)小分組。考慮到性能,每一個(gè)小分組都建議加上tagName,因此這樣在IE6方便使用documentsByTagName,比如p div.aaa 比 p.aaa快多了。前者兩次getElementsByTagName查找,最后用className過濾。后者是通過getElementsByTagName得到種子集,然后再取它們的所有子孫,明顯這樣得到的過濾集比前者數(shù)量多很多。
從實(shí)現(xiàn)上,你可以從左至右,也可以像sizzle那樣從右至左(大體上是,實(shí)際情況復(fù)雜很多)。
另外,選擇器也分為編譯型和非編譯型,編譯型是Ext發(fā)明的,這個(gè)陣營(yíng)的選擇器中有Ext,Qwrap,NWMatchers,JindoJS.非編譯型的就更多了,如Sizzle,Icarus,Slick,YUI,dojo,uupaa,peppy...
還有一些利用xpath實(shí)現(xiàn)的選擇器,最著名的是base2,它先實(shí)現(xiàn)了xpath那一套,方便IE也是有document,evaluate,然后將css選擇符翻譯成xpath。其它比較著名的有casperjs,DOMAssistant.
像sizzle mootools Icarus等還支持選擇XML元素(因?yàn)閄ML還是一種比較重要的數(shù)據(jù)傳輸格式。后端通過XHR返回我們的就可能是XML),這樣我們通過選擇器引擎抽取所需要的數(shù)據(jù)就簡(jiǎn)單多了。
4.選擇器引擎涉及的通用函數(shù)
1. isXML
最強(qiáng)大的前幾名選擇器引擎都能操作XML文檔,但XML與HTMl存在很大的差異,沒有className,getElementById,并且nodeName需要區(qū)分大小寫,在舊版IE中還不能直接給XML元素添加自定義屬性。因此,這些區(qū)分非常有必要。因此我們看一下各大引擎的實(shí)現(xiàn)吧。
Sizzle的實(shí)現(xiàn)如下。
var isXML = Sizzle.isXML = function (elem) {var documentElement = elem && (elem.ownDocument || elem).documentElement;return documentElement ? documentElement.nodeName !== "HTML" : false;};但這樣做不嚴(yán)謹(jǐn),因?yàn)閄ML的根節(jié)點(diǎn)可能是HTML標(biāo)簽,比如這樣創(chuàng)建一個(gè)XML文檔:
try{var doc = document.implementation.createDocument(null, 'HTML', null);alert(doc.documentElement)alert(isXML(doc))} catch (e) {alert("不支持creatDocument")}我們來看看mootools的slick的實(shí)現(xiàn)
isXML = function(document){return (!!document.xmlVersion) || (!!document.xml) || (toString.call(document) == '[object XMLDocument]') || (document.nodeType == 9 && document.documentElement.nodeName != 'HTML');};mootools用到了大量的屬性來進(jìn)行判定,從mootools1.2到現(xiàn)在還沒什么改動(dòng),說明還是很穩(wěn)定的。我們?cè)倬?jiǎn)一下。
在標(biāo)準(zhǔn)瀏覽器里,暴露了一個(gè)創(chuàng)建HTML文檔的構(gòu)造器HTMLDocument,而IE下的XML元素又擁有selectNodes:
var isXML = window.HTMLDocument ? function(doc) {return !(doc instanceof HTMLDocument)} : function (doc) {return "selectNodes" in doc}不過這些方法都是規(guī)范,javascript對(duì)象可以隨意添加,屬性法很容易被攻破,最好使用功能法。無論XML或HTML文檔都支持createElement方法,我們判定創(chuàng)建了元素的nodeName是否區(qū)分大小寫。
var isXML = function(doc) {return doc.createDocument("p").nodeName !== doc.createDocument("p").nodeName;}這是目前能給出最嚴(yán)謹(jǐn)?shù)暮瘮?shù)了。
2.contains
contains方法就是判定參數(shù)1是否包含參數(shù)2。這通常用于優(yōu)化。比如早期的Sizzle,對(duì)于#aaa p.class選擇符,它會(huì)優(yōu)先用getElementByClassName或getElementsByTagName取種子集,然后就不繼續(xù)往左走了,直接跑到最左的#aaa,取得#aaa元素,然后通過contains方法進(jìn)行過濾。隨著Sizzle體積進(jìn)行增大,它現(xiàn)在只剩下另一個(gè)關(guān)于ID的用法,即:如果上下文對(duì)象非文檔對(duì)象,那么它會(huì)取得其ownerDocument,這樣就可以用getElementById,然后用contains方法進(jìn)行驗(yàn)證!
//sizzle 1.10.15if (context.ownerDocument && (elem = context.ownerDocument.getElementById(m)) && contains(context, elem) && elem.id === m) {results.push(elem);return results;}contains的實(shí)現(xiàn)。
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;};它自己做了預(yù)判定,但這時(shí)傳入xml元素節(jié)點(diǎn),可能就會(huì)出錯(cuò),因此建議改成實(shí)時(shí)判定。雖然每次都進(jìn)入都判定一次那個(gè)原生API。
mass framework的實(shí)現(xiàn)方式:
//第一個(gè)節(jié)點(diǎn)是否包含第二個(gè)節(jié)點(diǎn),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;}現(xiàn)在來解釋一下contans與compareDocumentPosition這兩個(gè)API。contains原來是IE私有的,后來其他瀏覽器也借鑒這方法。如fireFox在9.0也安裝了此方法。它是一個(gè)元素節(jié)點(diǎn)的方法,如果另一個(gè)等于或包含它的內(nèi)部,就返回true.compareDocumentPosition是DOM的level3 specification定義的方法,firefox等標(biāo)準(zhǔn)瀏覽器都支持,它等于判定兩個(gè)節(jié)點(diǎn)的關(guān)系,而不但是包含關(guān)系。這里是NodeA.compareDocumentPosition(Node.B)包含你可以得到的信息。
| Bits | ? | ? |
| 000000 | 0 | 元素一致 |
| 000001 | 1 | 節(jié)點(diǎn)在不同的文檔(或者一個(gè)在文檔之外) |
| 000010 | 2 | 節(jié)點(diǎn)B在節(jié)點(diǎn)A之前 |
| 000100 | 4 | 節(jié)點(diǎn)A在節(jié)點(diǎn)B之前 |
| 001000 | 8 | 節(jié)點(diǎn)B包含節(jié)點(diǎn)A |
| 010000 | 16 | 節(jié)點(diǎn)A包含節(jié)點(diǎn)B |
| 100000 | 32 | 瀏覽器的私有使用 |
有時(shí)候,兩個(gè)元素的位置關(guān)系可能連續(xù)滿足上表的兩者情況,比如A包含B,并且A在B的前面,那么compareDocumentPosition就返回20.
舊版本的IE不支持compareDocumentPosition。jQuery作者john resig寫了個(gè)兼容函數(shù),用到IE的另一個(gè)私有實(shí)現(xiàn),sourceIndex, sourceIndex會(huì)根據(jù)元素位置的從上到下,從左至右依次加1,比如HTML標(biāo)簽的sourceIndex為0,Head的標(biāo)簽為1,Body的標(biāo)簽為2,Head的第一個(gè)子元素為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.節(jié)點(diǎn)的排序與去重
為了讓選擇器引擎搜到的結(jié)果集盡可能接近原生的API結(jié)果(因?yàn)樵谧钚碌臑g覽器中,我們可能只使用querySlelectorAll實(shí)現(xiàn)),我們需要讓元素節(jié)點(diǎn)按它們?cè)贒OM樹出現(xiàn)的順序排序。
IE早期的版本,我們可以使用sourceIndex進(jìn)行排序。
標(biāo)準(zhǔn)的瀏覽器可以使用compareDocumentPosition.在上小節(jié)中介紹了它可以判定兩個(gè)節(jié)點(diǎn)的位置關(guān)系。我們只要將它們的結(jié)果按位于4不等于0就知道其前后順序了。
var compare =comparerange.compareBoundaryPoints(how, sourceRange);compare:返回1,0,-1(0為相等,1為compareRange在sourceRange之后,-1為compareRange在sourceRange之前)
how:比較那些邊界點(diǎn):為常數(shù)
特別的情況發(fā)生于要兼容舊版本標(biāo)準(zhǔn)瀏覽器與XML文檔時(shí),這時(shí)只有一些很基礎(chǔ)的DOM API,我們需要使用nextSibling來判定誰是哥哥,誰是“弟弟”。如果他們不是同一個(gè)父節(jié)點(diǎn),我們就需要將問題轉(zhuǎn)化為求最近公共祖先,判定誰是“父親”,誰是"伯父"節(jié)點(diǎn)。
到這里,已經(jīng)是很單純的算法問題了。實(shí)現(xiàn)的思路有很多,最直觀最笨的做法是,不斷向上獲取他們的父節(jié)點(diǎn),直到HTML元素,連同最初的那個(gè)節(jié)點(diǎn),組成兩個(gè)數(shù)組,然后每次取數(shù)組最后的元素進(jìn)行比較,如果相同就去掉。一直取到不相同為止。最后用nextSibling結(jié)束。下面是測(cè)試的代碼。需要自己去試驗(yàn)。
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) {//查看調(diào)試消息window.console && window.console.log(s)}var sliceNodes = function(arr){//將NodeList轉(zhuǎn)化為純數(shù)組var ret = [],i = arr.length;while (i)ret [--i] = arr[i];return ret;}var sortNodes = function(a, b) {//節(jié)點(diǎn)排序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); //轉(zhuǎn)換成純數(shù)組log(els);els = shuffle(els); //洗牌的過程(模擬選擇器引擎最初得到的結(jié)果集的情況)log(els);els = els.sort(sortNodes); //進(jìn)行節(jié)點(diǎn)排序log(els) }?它沒打算支持xml與舊版標(biāo)準(zhǔn)瀏覽器,不支持就不會(huì)排序。
mass Framework的icarus引擎,結(jié)合了一位編程高手JK的算法,在排序去重遠(yuǎn)勝Sizzle。
其特點(diǎn)在于,無論sizzle或者slick,它們都是通過傳入比較函數(shù)進(jìn)行排序。而數(shù)組的原生sort方法,當(dāng)它傳一個(gè)比較函數(shù)時(shí),不管它內(nèi)部用哪種排序算法(ecma沒有規(guī)定sort的具體實(shí)現(xiàn)方法,因此,各個(gè)瀏覽器而異,比如FF2使用堆排序,FF3使用歸并排序,IE比較慢,具體的算法不明,可能為冒泡或插入排序,而chrome為了最大效率,采用了兩者算法:
http://yiminghe.iteye.com/blog/469713(玉伯大神) 附加一個(gè)排序方法:http://runjs.cn/code/tam0czbv
),都需要多次比對(duì),所以非常耗時(shí)間,如果能設(shè)計(jì)讓排序在不傳參的情況下進(jìn)行,那么速度就會(huì)提高N倍。
下面是具體的思路(當(dāng)然只能用于IE或早期的Opeara,所以代碼不貼出來。)
i.取出元素節(jié)點(diǎn)的sourceIndex值,轉(zhuǎn)換為一個(gè)String對(duì)象
ii.將元素節(jié)點(diǎn)附在String對(duì)象上
iii.用String對(duì)象組成數(shù)組
iiii.用原生的sor進(jìn)行string對(duì)象數(shù)組排序
iiiii.在排序好的String數(shù)組中,排序取出元素節(jié)點(diǎn),即可得到排序好的結(jié)果集。
在這里貼兩篇關(guān)于排序和節(jié)點(diǎn)選擇的前輩文章:擴(kuò)展閱讀
http://www.cnblogs.com/jkisjk/archive/2011/01/28/array_quickly_sortby.html
http://www.cnblogs.com/jkisjk/archive/2011/01/28/1946936.html
擴(kuò)展閱讀?(參考司徒先生的多代選擇器引擎:)http://www.cnblogs.com/rubylouvre/archive/2011/11/10/2243838.html
4.切割器
選擇器降低了javascript的入行門檻,它們?cè)谶x擇元素時(shí)都很隨意,一級(jí)一級(jí)地向上加ID類名,導(dǎo)致選擇符非常長(zhǎng),因此,如果不支持querySlelectorAll,沒有一個(gè)原生API能承擔(dān)這份工作,因此,我們通過使用正常用戶對(duì)選擇符進(jìn)行切割,這個(gè)步奏有點(diǎn)像編譯原理的詞法分析,拆分出有用的符號(hào)法來。
這里就拿Icarus的切割器來舉例,看它是怎么一步步優(yōu)化,就知道這工作需要多少細(xì)致。
/[\w\u00a1-\uFFFF][\w\u00a1-\uFFFF-]*|[#.:\[][\w\(\)\]]+|\s*[>+~,*]\s*|\s+/g比如,對(duì)于".td1,div a,body"上面的正則可完美將它分解為如下數(shù)組:
[".td1",",","div"," ","*",",","body"]
然后我們就可以根據(jù)這個(gè)符號(hào)流進(jìn)行工作。由于沒有指定上下文對(duì)象,就從document開始,發(fā)現(xiàn)第一個(gè)是類選擇器,可以用getElementsByClassName,如果沒有原生的,我們仿照一個(gè)也不是難事。然后是并聯(lián)選擇器,將上面得到的結(jié)果放進(jìn)結(jié)果集。接著是標(biāo)簽選擇器,使用getElementsByTgaName。接著是后代選擇器,這里可以優(yōu)化,我們可以預(yù)先查看一個(gè)選擇器群組是什么,發(fā)現(xiàn)是通配符選擇器,因此繼續(xù)使用getElementsByTgaName。接著又是并聯(lián)選擇器,將上面結(jié)果放入結(jié)果集。最后一個(gè)是標(biāo)簽選擇器,又使用getElementsByTgaName。最后是去重排序。
顯然,有切割好的符號(hào),工作簡(jiǎn)單多了。
但沒有東西一開始就是完美的,比如我們遇到一個(gè)這樣的選擇符,"nth-child(2n+1)".這是一個(gè)單獨(dú)的子元素過濾偽類,它不應(yīng)該在這里被分析。后面有專門的正則對(duì)它的偽類名與傳參進(jìn)行處理。在切割器里,它能得到最小的詞素是選擇器!
于是切割器進(jìn)行改進(jìn)
//讓小括號(hào)里邊的東西不被切割var reg = /[\w\u00a1-\uFFFF][\w\u00a1-\uFFFF-]*|[#.:\[](?:[\w\u00a1-\uFFFF-]|\([^\)]*\)|\])+|(?:\s*)[>+~,*](?:\s*)|\s+/g我們不斷增加測(cè)試樣例,我們問他越來越多,如這個(gè)選擇符 :“.td1[aa='>111']” ,在這種情況下,屬性選擇器被拆碎了!
[".td1","[aa",">","111"]于是正則改進(jìn)如下:
//確保屬性選擇器作為一個(gè)完整的詞素var reg = /[\w\u00a1-\uFFFF][\w\u00a1-\uFFFF-]*|[#.:](?:[\w\u00a1-\uFFFF-]|\S*\([^\)]*\))+|\[[^\]]*\]|(?:\s*)[>+~,*](?:\s*)|\s+/g對(duì)于選擇符"td + div span",如果最后有一大堆空白,會(huì)導(dǎo)致解析錯(cuò)誤,我們確保后代選擇器夾在兩個(gè)選擇器之間
["td", "+", "div", " ", "span", " "]
最后一個(gè)選擇器會(huì)被我們的引擎認(rèn)作是后代選擇器,需要提前去掉
//縮小后迭代選擇器的范圍var reg = /[\w\u00a1-\uFFFF][\w\u00a1-\uFFFF-]*|[#.:](?:[\w\u00a1-\uFFFF-]|\S+\([^\)]*\))+|\[[^\]]*\]|(?:\s*)[>+~,*](?:\s)|\s(?=[\w\u00a1-\uFFFF*#.[:])/g如果我們也想將前面的空白去掉,可能不是一個(gè)單獨(dú)的正則能做到的。現(xiàn)在切割器已經(jīng)被我們搞的相當(dāng)復(fù)雜了。維護(hù)性很差。在mootools中等引擎中,里邊的正則表達(dá)式更加復(fù)雜,可能是用工具生成的 。到了這個(gè)地方,我們需要轉(zhuǎn)換思路,將切割器該為一個(gè)函數(shù)處理。當(dāng)然,它里邊也少了不少正則。正則是處理字符串的利器。
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 = []; //這里放置切割單個(gè)選擇器群組得到的詞素,以,為界var parts = []; //這里放置切割單個(gè)選擇器組得到的詞素,以關(guān)系選擇器為界do {expr = expr.replace(reg_split,function(part) {if (part === ",") { //這個(gè)切割器只處理到一個(gè)并聯(lián)選擇器flag_break = true;} else {if (part.match(slim)) { //對(duì)于關(guān)系并聯(lián)。通配符選擇器兩邊的空白進(jìn)行處理//對(duì)parts進(jìn)行反轉(zhuǎn),例如 div.aaa,反轉(zhuǎn)先處理aaafull = full.concat(parts.reverse(),part.replace(/\s/g, ''));parts = [];} else {parts[parts.length] = part}}return "";//去掉已經(jīng)處理了的部分});if (flag_break) break;} while (expr)full =full.concat(parts.reverse());!full[0] && full.shift(); //去掉開頭的第一個(gè)空白return full;} var expr = " div > div#aaa,span"console.log(spliter(expr)); //=> ["div", ">", "div"]當(dāng)然,這個(gè)相對(duì)于sizzle1.8與slick等引擎來說,不值一提,需要有非常深厚的正則表達(dá)式功力,深層的知識(shí)寶庫自動(dòng)機(jī)理論才能寫出來。
5.屬性選擇器對(duì)于空白字符的匹配策略
上文已經(jīng)介紹過屬性選擇器的七種形態(tài)了,但屬性選擇器并沒有這么簡(jiǎn)單,在w3c草案對(duì)屬性選擇器[att~=val]提到了一個(gè)點(diǎn),val不能為空白字符,否則比較值flag(flag為val與元素實(shí)際值比較結(jié)果)返回false。如果querySlelectorAll測(cè)試一下屬性其他狀態(tài),我們會(huì)得到更多類似結(jié)果。
<!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.子元素過濾偽類的分級(jí)與匹配
子元素過濾偽類是css3新增的一種選擇器。比較復(fù)雜,這里單獨(dú)放出來說。首先,我們要將它從選擇符中分離出來。這個(gè)一般由切割器搞定。然后我們用正則將偽類名與它小括號(hào)里的傳參分解出來。
如下是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;//繼續(xù)取它左邊的部分重寫exprif ( !! ~key.indexOf("nth")) { //如果子元素過濾偽類args = parseNth[match[1]] || parseNth(match[1]);//分解小括號(hào)的傳參} else {args = match[3] || match [2] || match[1]}break;default://屬性選擇器 略break;}這里有個(gè)小技巧,我們需要不斷把處理過的部分從選擇器中去掉。一般選擇器引擎是使用expr = expr.replace(reg,"")進(jìn)行處理,Icarus巧妙的使用正則的RegExp.rightContext進(jìn)行復(fù)寫,將小括號(hào)里邊的字符串取得我們通過parseNTH進(jìn)行加工。將數(shù)字1,4,單詞even,odd,-n+1等各種形態(tài)轉(zhuǎn)換為an+b的形態(tài)。
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是一個(gè)緩存函數(shù),這樣能避免重復(fù)解析,提高引擎的總體性能(緩存的精髓)
關(guān)于緩存的利用,可以參看Icarus Sizzle1.8+ Slice等引擎,需求自己可尋找。
?5.sizzle引擎
jQuery最大的特點(diǎn)就是其選擇器,jQuery1.3開始裝其Sizzle引擎。sizzle引擎與當(dāng)時(shí)主流的引擎大不一樣,人們說它是從右至左選擇(雖然不對(duì),但大致方向如此),速度遠(yuǎn)勝當(dāng)時(shí)選擇器(不過當(dāng)時(shí)也沒有什么選擇器,因此sizzle一直自娛自樂)
Sizzle當(dāng)時(shí)有以下幾個(gè)特點(diǎn)
i允許關(guān)系選擇器開頭
ii允許反選擇器套取反選擇器
iii大量的自定義偽類,比如位置偽類(eq,:first:even....),內(nèi)容偽類(:contains),包含偽類(:has),標(biāo)簽偽類(:radio,:input,:text,:file...),可見性偽類(:hidden,:visible)
iiii對(duì)結(jié)果進(jìn)行去重,以元素在DOM樹的位置進(jìn)行排序,這樣與未來出現(xiàn)的querySelector行為一致。
?顯然,搞出這么東西,不是一朝半夕的事情,說明john Resig研發(fā)了很久。當(dāng)時(shí)sizzle的版本號(hào)為0.9.1,代碼風(fēng)格跟jQuery大不一樣,非常整齊清晰。這個(gè)風(fēng)格一直延續(xù)到j(luò)Query.1.7.2,Sizzle版本也跟上為1.7.2,在jQuery.1.8時(shí),或sizzle1.8時(shí),風(fēng)格大變,首先里邊的正則式是通過編譯得到的,以求更加準(zhǔn)確,結(jié)構(gòu)也異常復(fù)雜,開始走ext那樣編譯函數(shù)的路子,通過多種緩存手段提高查詢速度和匹配速度。
由于第二階段的Sizzle蛻變還沒有完成,每星期都在變,tokenize ,addMatcher,matcherFrom,Tokens,matcherFormGroupMachers,complile這些關(guān)鍵的內(nèi)部函數(shù)都在改進(jìn),不斷膨脹。我們看是來看看sizzle1.72,這個(gè)版本是john Resing第一個(gè)階段的最完美的思想結(jié)晶。
當(dāng)時(shí),Sizzle的整體結(jié)構(gòu)如下:
i.Sizzle主函數(shù),里邊包含選擇符的切割,內(nèi)部循環(huán)調(diào)用住查找函數(shù),主過濾函數(shù),最后是去重過濾。
ii.其它輔助函數(shù),如uniqueSort, matches, matchesSelector
iii.Sizzle.find主查找函數(shù)
iiii.Sizzle.filiter過濾函數(shù)
iiiii.Sizzle.selectors包含各種匹配用的正則,過濾用的正則,分解用的正則,預(yù)處理用的函數(shù),過濾函數(shù)等
iiiiii.根據(jù)瀏覽器特征設(shè)計(jì)makeArray,sortOrder,contains等方法
iiiiiii.根據(jù)瀏覽器特征重寫Sizzle.selectors中的部分查找函數(shù),過濾函數(shù),查找次序。
iiiiiiii.若瀏覽器支持querySelectorAll,那么用它重寫Sizzle,將原來的sizzle作為后備方案包裹在新的sizzle里邊
iiiiiiiii.其它輔助函數(shù),如isXML,posProcess
?下面使用一部分源碼分析下1.7.2sizzle
var Sizzle = function(slelctor, context, results, seed) {//通過短路運(yùn)算符,設(shè)置一些默認(rèn)值results = results || [];context = context || document;//備份,因?yàn)閏ontext會(huì)被改寫,如果出現(xiàn)并聯(lián)選擇器,就無法確保當(dāng)前節(jié)點(diǎn)是對(duì)于哪一個(gè)contextvar origContext = context;//上下文對(duì)象必須是元素節(jié)點(diǎn)或文檔對(duì)象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;//下面是切割器的實(shí)現(xiàn),每次只處理到并聯(lián)選擇器,extra給留下次遞歸自身時(shí)作傳參//不過與其他引擎實(shí)現(xiàn)不同的是,它沒有一下子切成選擇器,而且切成選擇器組與關(guān)系選擇器的集合//比如body div > div:not(.aaa),title//后代選擇器雖然被忽略了,但在循環(huán)這個(gè)數(shù)組時(shí),它默認(rèn)每?jī)蓚€(gè)選擇器組與關(guān)系選擇器不存在就放在后代選擇器到那個(gè)位置上do {chunker.exec(""); //這一步主要講chunker的lastIndex重置,當(dāng)然是直接設(shè)置chunker.lastIndex效果也一樣m = chunker.exec(soFar);if (m) {soFar = m[3];parts.push(m[1]);if (m[2]) { //如果存在并聯(lián)選擇器,就中斷extra = m[3];break;}}} while (m);// 略....}接下來有許多分支,分別是對(duì)ID與位置偽類進(jìn)行優(yōu)化的(暫時(shí)跳過),著重幾個(gè)重要概念,查找函數(shù),種子集,映射集。這里集合sizzle源碼。
查找函數(shù)就是Sizzle.selecters.find下的幾種函數(shù),常規(guī)情況下有ID,TAG,NAME三個(gè),如果瀏覽器支持getElementByClassName,還會(huì)有Class函數(shù)。正如我們前面所介紹的那樣,getElementById,geyElementsByName,getElementsByTagName,geyElementByClassName不能完全信任他們,即便是標(biāo)準(zhǔn)瀏覽器都會(huì)有bug,因此四大查找函數(shù)都做了一層封裝,不支持返回undefined,其它則返回?cái)?shù)組NodeList
種子集,或叫候選集,就是通過最右邊的選擇器組得到的元素集合,比如說"div.aaa span.bbb",最右邊的選擇器組就是"span.bbb" ,這時(shí)引擎就會(huì)根據(jù)瀏覽器支持的情況選擇getElemntByTagName或者getElementClassName得到一組元素,然后通過className或tagName進(jìn)行過濾。這時(shí)得到的集合就是種子集,Sizzle的變量名seed就體現(xiàn)了這一點(diǎn)
映射集,或叫影子集,Sizzle源碼的變量名為checkSet。這是個(gè)怎樣的東西呢?當(dāng)我們?nèi)〉梅N子集后,而是將種子集賦值一份出來,這就是映射集。種子集是由選擇器組選出來的,這時(shí)選擇符不為空,必然往左就是關(guān)系選擇器。關(guān)系選擇器會(huì)讓引擎去選其兄長(zhǎng)或父親(具體參見Sizzle.selector.relative下的四大函數(shù)),把這些元素置換到候選集對(duì)等的位置上。然后到下一個(gè)選擇器組時(shí),就是純過濾操作。主過濾函數(shù)sizzle.filter會(huì)調(diào)用sizzle.seletors下N個(gè)過濾函數(shù)對(duì)這些元素進(jìn)行監(jiān)測(cè),將不符合的元素替換為false.因此,到最后去重排序時(shí),映射集是一個(gè)包含布爾值與元素節(jié)點(diǎn)的數(shù)組(true也是在這個(gè)步奏中產(chǎn)生的)。
種子集是分兩步選擇出來的。?首先,通過Sizzle.find得到一個(gè)大致的結(jié)果。然后通過Sizzle.filter,傳入最右那個(gè)選擇器組剩余的部分做參數(shù),縮小范圍。
//這是征對(duì)最左邊的選擇器組存在ID做出的優(yōu)化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)//這里會(huì)對(duì)~,+進(jìn)行優(yōu)化,直接取它的上一級(jí)做上下文} : 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呢?這里有個(gè)準(zhǔn)則,確保我們后面的映射集最小化。直白的說,映射集里邊的元素越少,那么調(diào)用的過濾函數(shù)的次數(shù)就越少,說明進(jìn)入另一個(gè)函數(shù)作用域所造成的耗時(shí)就越少,從而整體提高引擎的選擇速度。
為了達(dá)到此目的,這里做了一個(gè)優(yōu)化,原生選擇器的調(diào)用順序被放到了一個(gè)叫Sizzle.selector.order的數(shù)組中,對(duì)于陳舊的瀏覽器,其順序?yàn)镮D,NANME,TAG,對(duì)于支持getElementByClassName的瀏覽器,其順序?yàn)镮D,Class,NAME,TAG。因?yàn)?#xff0c;ID至多返回一個(gè)元素節(jié)點(diǎn),ClassName與樣式息息相關(guān),不是每個(gè)元素都有這個(gè)類名,name屬性帶來的限制可能比className更大,但用到的幾率較少,而tagName可排除的元素則更少了。
那么Sizzle.find就會(huì)根據(jù)上面的數(shù)組,取得它的名字,依次調(diào)用Sizzle.leftMatch對(duì)應(yīng)的正則,從最右的選擇器組切割下需要的部分,將換行符處理掉,通過四大查找函數(shù)得到一個(gè)粗糙的節(jié)點(diǎn)集合。如果得到"[href=aaa]:visible"這樣的選擇符,那么只有把文檔中所有節(jié)點(diǎn)作為結(jié)果返回。
//sizzle.find為主查找函數(shù)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) { //沒有的話,尋找該上下文對(duì)象的所有子孫set = typeof context.getElementsByTagName !== "undefined" ? context.getElementsByTagName("*") : [];}return {set: set,expr : expr};};經(jīng)過主查找函數(shù)處理后,我們得到一個(gè)初步的結(jié)果,這時(shí)最右邊的選擇器可能還有殘余,比如“div span.aaa”可能余下"div span","div .aaa.bbb"可能余下“div .bbb”,這個(gè)轉(zhuǎn)交主過濾函數(shù)Sizzle.filter函數(shù)處理。
它有兩種不同的功能,一是不斷的縮小集合的個(gè)數(shù),構(gòu)成種子集返回。另一種是將原集合中不匹配的元素置換為false。這個(gè)根據(jù)它的第三個(gè)傳參inplace而定。
Sizzle.filter = function (expr, set, inplace, not) {//用于生成種子集或映射集,視第三個(gè)參數(shù)而定//expr: 選擇符//set: 元素?cái)?shù)組//inplace: undefined, null時(shí)進(jìn)入種子集模式,true時(shí)進(jìn)入映射集模式//not: 一個(gè)布爾值,來源自去反選擇器.....}?待我們把最右邊的選擇器組的最后都去掉后,種子集宣告完成,然后處理下一個(gè)選擇器組,并將種子集復(fù)制一下,生成映射集。在關(guān)系選擇器4個(gè)對(duì)應(yīng)函數(shù)———他們?yōu)镾izzle.selectors.relative命名空間下————只是將映射集里邊的元素置換為它們的兄長(zhǎng)父親,個(gè)數(shù)是不變。因此映射集與種子集的數(shù)量總是相當(dāng)。另外,這四個(gè)函數(shù)內(nèi)部也在調(diào)用Sizzle.filter函數(shù),它們的inplace參數(shù)為true,走映射集的邏輯。
如果存在并聯(lián)選擇器,那就再調(diào)用Sizzle主函數(shù),把得到兩個(gè)結(jié)果合并去重
if (extara) {Sizzle(extra, origContext, results, seed);Sizzle.uniqueSort(result) }
這個(gè)過程就是Sizzle的主流程。下面將是根據(jù)瀏覽器的特性優(yōu)化或調(diào)整的部分。比如ie6 7下的getElementById有bug,需要沖洗Expr.find.ID與Expr.filterID.ie6-ie8下,Array.prototype.slice.call無法切割NodeList,需要從寫makeArray.IE6-8下,getElementsByTagName("*")會(huì)混雜在注釋節(jié)點(diǎn),需要從寫Expr.find.TAG,如果瀏覽器支持querySelectorAll,那么需要重寫整個(gè)Sizzle.
下面就從寫個(gè)瀏覽器支持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文檔,在標(biāo)準(zhǔn)瀏覽器XML文檔中實(shí)現(xiàn)了接口,但不工作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//優(yōu)化只有單個(gè)標(biāo)簽選擇器的情況if (match[1]) {return makeArray(context.getElementsByTagName(query), extra);//優(yōu)化只有單個(gè)類選擇的情況} else if (match[2] && Expr.find.CLASS && context.getElementsByClassName) {return makeArray(context.getElementsByClassName(match[2]), extra);}}if (context.nodeType === 9) { //文檔Document//優(yōu)化選擇符為body的情況//因?yàn)槲臋n只有它一個(gè)標(biāo)簽,并且對(duì)于屬性直接取它if (query === "body" && context.body) {return makeArray ([context.body], extra);//優(yōu)化只有ID選擇器的情況//speed-up: Sizzle("ID")} else if (match && match[3]) {var elem = context.getElementById(match[3]);//注意,瀏覽器也會(huì)優(yōu)化,它會(huì)緩存了上次的結(jié)果//即便他現(xiàn)在移除了DOM樹if (elem && elem.parentNode) {//ie和opera會(huì)混淆id和name,確保id等于目標(biāo)值if (elem.id === match[3]) {return makeArray([elem], extra);}} else {return makeArray([], extra);}}try {return makeArray(context.querySelectorAll(query), extra);} catch (queryError) {}//ie8下的querySelectorAll實(shí)現(xiàn)存在bug,它會(huì)包含自己的集合內(nèi)查找符合自己的元素節(jié)點(diǎn)//根據(jù)規(guī)范,應(yīng)該是在當(dāng)前上下文中的所有子孫元素下查找,IE8下如果元素節(jié)點(diǎn)為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取出來放到這個(gè)分組的最前面,比如div b --> [id=xxx] div b//不存在id,就創(chuàng)建一個(gè),重復(fù)上面的操作,最后會(huì)刪掉這個(gè)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函數(shù)上for (var prop in oldSizzle) {Sizzle[prop] = oldSizzle[prop];}//release memory in IEdiv = null})();}?從源碼中可以看出,它不單單是重寫那么簡(jiǎn)單,根據(jù)不同的情況還有各種提速方案。getElementById自不用說,速度肯定快,這內(nèi)部做了緩存,而且getElementById最多只返回一個(gè)元素節(jié)點(diǎn),而querySelectorAll則會(huì)返回?fù)碛羞@個(gè)ID值的多個(gè)元素。這個(gè)聽起來有點(diǎn)奇怪,querySelectorAll不會(huì)理會(huì)你的錯(cuò)誤行為,機(jī)械執(zhí)行指令。
另外getElementsByTagName也是內(nèi)部使用了緩存,它也比querySelectorAll快,getElementsByTagName返回的是一個(gè)NodeList對(duì)象,而querySelectorAll返回的是一個(gè)StaticNodeList對(duì)象。一個(gè)是動(dòng)態(tài)的,一個(gè)是靜態(tài)的。
測(cè)試下不同:
var tag = "getElementsByTagName", sqa = "querySelectorAll";console.log(document[tag]("div") == document[tag]("div")); //=>trueconsole.log(document[sqa]("div") == document[sqa]("div")); //=>falseps:true意味著它們拿到的同是cache引用,Static每次返回都是不一樣的Object
往上有數(shù)據(jù)表明,創(chuàng)建一個(gè)動(dòng)態(tài)的NodeList對(duì)象比一個(gè)靜態(tài)的StaticNodeList對(duì)象快90%
querySelectorAll的問題遠(yuǎn)不至與此,IE8中微軟搶先實(shí)現(xiàn)了它,那時(shí)征對(duì)它的規(guī)范還沒有完成,因此不明確的地方微軟自行發(fā)揮了。IE8下如果對(duì)StaticNodeList取下標(biāo)超界,不會(huì)返回undefined,而是拋出異常 Invalid ?procedure call or argument。
var els = document.body.querySelectorAll('div');alert(els[2])//2> els.length-1因此,一些奇特的循環(huán),要適可而止。
最后,說明下querySelectorAll這個(gè)API在不同的瀏覽器。不同的版本中都存在bug,不支持的選擇器類型多了,我們需要在工作中做充分的測(cè)試(Sizzle1.9中,中槍的偽類就有focus , :enabled , :disabled , :checked.....)。
在現(xiàn)實(shí)工作中,想要支持選擇器類型越多,就需要在結(jié)構(gòu)上設(shè)計(jì)的有擴(kuò)展性。但過分添加直接的定義偽類,就意味著未來與querySelectorAll發(fā)生的沖突越多。像zopto.js,就是一個(gè)querySelectorAll當(dāng)成自己選擇器的引擎,Sizzle1.9時(shí)已經(jīng)有1700行代碼。最近jQuery也做了一個(gè)selector-native模塊,審視未來。
轉(zhuǎn)載于:https://www.cnblogs.com/wingzw/p/7360236.html
總結(jié)
- 上一篇: 服务器宕机原因
- 下一篇: 城市三维地下管线管理系统 (转载)