第15章 CSS選擇引擎 {#15-css}
隨著web開發(fā)越來越專業(yè),所有流行的瀏覽器都包含了W3C選擇器API吠谢。這個API提供了querySelectorAll()和querySelector()兩個方法合是,在我們應(yīng)用中結(jié)合其它一些好的工具可以寫出滿足跨瀏覽器要求的可以快速遍歷DOM的代碼火窒。
注意 想要得到更多有關(guān)API信息么甚垦?請查看W3C Level1(www.w3.org/TR/selectors-api/)和W3C Level2(www.w3.org/TR/selectors-api2/)。
你可能會問释液,幾乎在所有的瀏覽器中都實(shí)現(xiàn)了W3C選擇器API全释,為什么我們還要花時間來討論如何實(shí)現(xiàn)純JavaScript的CSS選擇器引擎呢?
雖然存在這些標(biāo)準(zhǔn)的API是件好事误债,但是大多數(shù)瀏覽器只是生硬地實(shí)現(xiàn)了浸船。這樣的做法就與一個好的API不沾邊了。例如找前,這樣方法不能利用已經(jīng)創(chuàng)建好的DOM緩存糟袁,它們不能提供錯誤報告,它們也不能處理任何形式的擴(kuò)展躺盛。
流行的JavaScript類庫中的CSS選擇器考慮到了這些因素。他們使用DOM緩存提供更高的性能形帮,它們提供了錯誤報告槽惫,它們擁有很高的擴(kuò)展性。
提示 CSS選擇器引擎是一個功能性的術(shù)語辩撑,它通過CSS表達(dá)式來匹配DOM元素界斜,例如,.ninja的表達(dá)式可以匹配所有class為ninja的元素合冀。
說了這么多各薇,還是沒有回答問題:為什么需要理解純JavaScript的CSS選擇器引擎是如何工作的呢?理解這些當(dāng)然是為了驚人的性能收益了。這樣做不只是可以實(shí)現(xiàn)更快峭判、更好的遍歷方法开缎,也可以讓我了解到如何創(chuàng)建適應(yīng)CSS選擇器引擎的更高性能的CSS選擇器。
CSS選擇器引擎如今是日常開發(fā)中的一部分林螃,理解它們?nèi)绾喂ぷ鬓壬尽⑷绾问顾鼈児ぷ鞲炜梢栽陂_發(fā)為我們提供基本的幫助。如果你在捉摸我們要如何做疗认,主要遵循以下模式:
- 查找DOM元素完残。
- 對它們做一些事情。
除了新的選擇器API横漏,查找DOM元素從來都不是重點(diǎn)谨设。這些API對于使用ID或者標(biāo)簽名查找API有限制。不管怎么說缎浇,第一步做起來是比較簡單的铝宵,讓我們把重點(diǎn)放在第二步上。
CSS3選擇器引擎標(biāo)準(zhǔn)被W3C定義在www.w3.org/TR/css3-selectors/华畏。
有三種重要的實(shí)現(xiàn)CSS選擇器引擎的方式:
- 使用之前提到的W3C選擇器API鹏秋。
- 使用許多瀏覽器中內(nèi)置的XPath語言
- 使用純粹的DOM,如果前兩個功能不存在亡笑,它就是主要的CSS選擇器引擎
本章將會深入地探討這些策略侣夷,可以允許我們決定去實(shí)現(xiàn)還是理解一個JavaScript CSS選擇器引擎。
我們將從W3C的方法開始仑乌。
15.1 W3C選擇器API {#15-1-w3c-api}
W3C選擇器API是一個比較新的API百拓,在JavaScript中使用它實(shí)現(xiàn)完整的CSS選擇器引擎可以減少許多工作量。
瀏覽器供應(yīng)商抓住了這個新的API晰甚,并且在大多數(shù)瀏覽器中實(shí)現(xiàn)了它(safari 3衙传,IE8,Chrome厕九,和Opera10)蓖捶。API一般情況下支持所有CSS引擎實(shí)現(xiàn)的選擇器,因此一個瀏覽器是否完全支持CSS3扁远,從它的選擇器的實(shí)現(xiàn)上就可以反映出來俊鱼。
API提供了一些有用的方法,它們其中的兩個在流行的瀏覽器中已經(jīng)實(shí)現(xiàn)了:
- querySelector() 接收一個CSS選擇器字符串并且返回第一個元素畅买,如果沒有找到返回null并闲。
- querySelectorAll() 接收一個CSS選擇器字符串并且返回一個靜態(tài)的NodeList,其中包含查找到的所有元素谷羞。
在兩個方法在所有的DOM元素帝火,DOM文檔,DOM框架中存在。
下面的代碼展示如何使用這些API犀填。
Listing 15.1 Examples of the Selectors API in action
<div id="test">
<b>Hello</b>, I'm a ninja!
</div>
<div id="test2"></div>
<script type="text/javascript">
window.onload = function () {
//Finds <div> elements that are children of the body
var divs = document.querySelectorAll("body > div");
assert(divs.length === 2, "Two divs found using a CSS selector.");
//Finds only children who are bold!
var b = document.getElementById("test")
.querySelector("b:only-child");
assert(b,
"The bold element was found relative to another element.");
};
</script>
對比其它JavaScript類庫的實(shí)現(xiàn)蠢壹,瀏覽器實(shí)現(xiàn)的API只在有限的程度上支持了CSS選擇器。這個問題可以在根元素的查詢規(guī)則中體現(xiàn)出來(調(diào)用querySelector()和querySelectorAll()都可以)宏浩。
Listing 15.2 Element-rooted queries
<div id="test">
<b>Hello</b>, I'm a ninja!
</div>
<script type="text/javascript">
window.onload = function () {
var b = document.getElementById("test").querySelector("div b");
assert(b, "Only the last part of the selector matters.");
};
</script>
注意:執(zhí)行這個查詢時知残,選擇器只會在元素內(nèi)部查找與表達(dá)式中最后一部分匹配的元素。這個看起來與直覺想違背的比庄。在listing 15.2中求妹,我們可以看到在id為test的元素中并沒有div,但是這個選擇器卻是成功的佳窑。
這種情況與大多數(shù)人想像的都不一樣制恍,所以我們需要提供一個解決方案。最常見的方法是為根節(jié)點(diǎn)元素提供一個新的id神凑,更改它的上下文净神。
Listing 15.3 Enforcing the element root
<div id="test">
<b>Hello</b>, I'm a ninja!
</div>
<script type="text/javascript">
(function() {
//使用立即執(zhí)行方法將計數(shù)器變量綁定到rootedQuerySelectorAll()方法上
var count = 1;
// 將方法定義到全局定義域上
this.rootedQuerySelectorAll = function (elem, query) {
// 保存原始的id,之后我們再將它設(shè)置回來
var oldID = elem.id;
// 分配唯一的臨時
elem.id = "rooted" + (count++);
try {
return elem.querySelectorAll("#" + elem.id + " " + query);
}
catch (e) {
throw e;
}
finally {
//在finally模塊中重置原始的id溉委,這樣可以保證代碼永遠(yuǎn)會執(zhí)行
elem.id = oldID;
}
};
})();
window.onload = function () {
var b = rootedQuerySelectorAll(
document.getElementById("test"), "div b");
assert(b.length === 0, "The selector is now rooted properly.");
};
</script>
在listing15.3中有兩點(diǎn)比較重要鹃唯。
一開始,我們必須為元素分配一個唯一的id瓣喊,并且存儲之前的id坡慌。這樣是為了保證在我們創(chuàng)建選擇器的時候得到的結(jié)果不會產(chǎn)生沖突。然后我們將這個id預(yù)置到選擇器之前(形式是“#id”藻三,id是唯一值)洪橘。
程序會簡單地移除id并且返回查詢結(jié)果。但是注意一點(diǎn):選擇器API可以拋出異常(通常是選擇器語法錯誤或者不支持選擇器)棵帽∠ㄇ螅基于這個原因,我們選擇try/catch語法逗概。由于我們想要重設(shè)id弟晚,我們可以添加一個額外的finally模塊。這是語言本身的特性:即便我們在try中返回或者在catch中拋出異常仗谆,代碼最終都會執(zhí)行finally模塊(執(zhí)行會早于當(dāng)前方法的返回)指巡。通過這種方法id總是被設(shè)置正確的。
選擇器API是W3C提出最有前途的API之一隶垮。在支持這種API的瀏覽器主導(dǎo)市場后,它就可以通過很簡單的方法替代大部分JavaScript類庫中的大部分工作了秘噪。
轉(zhuǎn)移注意力狸吞,讓我們以XML的方式解決這個問題。
15.2 使用XPath查找元素 {#15-2-xpath}
對于那些不支持選擇器API的瀏覽器,統(tǒng)一地使用XPath查詢蹋偏。
XPath是在文檔中查找元素的語言便斥。它明顯地要強(qiáng)于傳統(tǒng)的CSS選擇器,大多數(shù)流行的瀏覽器(Firefox威始, Safari 3+枢纠,Opera 9+,Chrome)都支持了XPath黎棠,它可以應(yīng)付基于HTML的DOM文檔晋渺。IE6及以上提供支持XML文檔的XPath(并不是HTML文檔——它是我們共同的目標(biāo))。
使用XPath表達(dá)式替代純粹的DOM操作能夠得到的收益并沒有一個確定的標(biāo)準(zhǔn)脓斩。由于編程的方式這個標(biāo)準(zhǔn)被確定后木西,依然需要知道一些事情:通過純粹的DOM方法使用id或者標(biāo)簽來查找元素仍然是比較快的(使用getElementById()和getElementByTagName())。
如果我們面對的觀眾可以舒服地使用XPath表達(dá)式(并且很欣然地將自己限制在主流的瀏覽器上)随静,我們就可以下面這段代碼(Prototype類庫提供)并且完全忽略任何關(guān)于CSS選擇器引擎的東西八千。
Listing 15.4 A method for executing an XPath expression on an HTML document
if (typeof document.evaluate === "function") {
function getElementsByXPath(expression, parentElement) {
var results = [];
var query = document.evaluate(expression, parentElement || document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (var i = 0, length = query.snapshotLength; i < length; i++)
results.push(query.snapshotItem(i));
return results;
}
}
使用XPath可以很好的處理所有事情,但是實(shí)際上根本不太可行燎猛。XPath封裝的這些功能是為開發(fā)人員使用的恋捆,并且相對于CSS選擇器而言它過于復(fù)雜了。我們在這里不能看到完整的XPath重绷,但是table 15.1提供了最常用的XPath表達(dá)式沸停,并且提供了與它們對應(yīng)的CSS選擇器。
Table 15.1 CSS selectors and their equivalent XPath expressions
Goal | XPath | CSS3 |
---|---|---|
All elements | //* | * |
All elements named p | //p | P |
All immediate child elements of p | //p/* | P > * |
Element by ID | //*[@id=’foo’] | #foo |
Element by Class | //*[contains(concat(“”,@class,””),”foo”)] | .foo |
Element with attribute | //*[@title] | *[title] |
First child of all p | //p/*[0] | P > *:first-child |
All p with an a descendant | //p[a] | Not possible |
Next element | //p/following-slibling::*[0] | P + * |
相對于使用純粹的DOM選擇器引擎论寨,我可以使用XPath表達(dá)式創(chuàng)建選擇器引擎星立,并且使用正則轉(zhuǎn)換選擇器。不同之處是將CSS選擇器映射為與之對應(yīng)的XPath表達(dá)式葬凳,然后執(zhí)行绰垂。
因為大量的DOM CSS選擇器引擎的實(shí)現(xiàn),這個方法并沒有帶來太多有利于我們的東西火焰。許多開發(fā)者不使用XPath是為了減少他們引擎的復(fù)雜程度劲装。你需要權(quán)衡使用XPath引擎帶來的收益(特別是對比選擇器API)。
現(xiàn)在開始卷起我們的衣袖昌简。
15.3 純DOM實(shí)現(xiàn) {#15-3-dom}
在每個CSS選擇器引擎的核心都是一個純粹的DOM實(shí)現(xiàn)占业。它的要求是轉(zhuǎn)換CSS選擇器并且使用已存在的DOM方法(getElementById()
與getElementByTagName()
)查找符合的元素。
注意 HTML5在標(biāo)準(zhǔn)方法中添加了getElementsByClassName()
方法纯赎。
使用DOM實(shí)現(xiàn)CSS選擇器有一些重要的原因:
- IE6谦疾、7——IE8和9已經(jīng)支持了
querySelectorAll()
,IE6和IE7卻漏掉了XPath或者選擇器API犬金,所以使用DOM實(shí)現(xiàn)就變得必須了散休。 - 向后兼容——如果你想要你的代碼優(yōu)雅降級并且支持那些不支持選擇器API或者XPath的瀏覽器(比如Safari 2),你就需要使用DOM形式來實(shí)現(xiàn)尸执。
- 速度——有許多純粹的DOM選擇器性能非常好(例如使用ID查找元素)。
- 完全覆蓋——并不是所有的瀏覽器都支持相同集合的CSS選擇器疗疟。如果我們想要在所有瀏覽器中支持所有這些CSS選擇器集合——或者至少是那些最常用的,我們就需要憑借自己的力量了瞳氓。
牢記這些策彤,我們可以想到兩個可能的CSS選擇器引擎的實(shí)現(xiàn):從上到下和從下到上。
從上到下的選擇器引擎將CSS選擇器從左至右進(jìn)行解析匣摘,然后根據(jù)每個選擇器片段匹配元素店诗。在大多數(shù)JavaScript類庫中都可以找到這種引擎,查找元素時優(yōu)先使用這種工具恋沃。
看下面這個簡單的例子:
<body>
<div></div>
<div class='ninja'>
<span>Please </span><a href='/ninja'><span>Click me!<span></a>
</div>
<body>
如果我們想要找到包含“Click me!”的<span>必搞,我們可以使用下面這個選擇器:
div.ninja a span
從上到下的方法在Figure 15.1中已經(jīng)描繪了。
第一步囊咏,div.ninja標(biāo)識了document中的子樹恕洲。下一步,在子樹中使用a標(biāo)識了錨點(diǎn)元素梅割。最后span標(biāo)識了最終的目標(biāo)元素霜第。注意,這是一個簡單的例子户辞。在任何階段都可以標(biāo)識復(fù)雜的子樹泌类。
當(dāng)開發(fā)選擇器引擎時需要考慮兩個重要的注意事項:
- 這些結(jié)果需要按照文檔的順序(它們被定義時的順序)。
- 這些結(jié)果應(yīng)該是唯一的(不應(yīng)該返回重復(fù)的元素)底燎。
出于這些方面的考慮刃榨,開發(fā)從上到下的引擎是比較復(fù)雜的。
來看一個簡化的實(shí)現(xiàn)双仍,使用標(biāo)簽名查找元素枢希。
Listing 15.5 A limited, top-down selector engine
<div>
<div>
<span>Span</span>
</div>
</div>
<script type="text/javascript">
window.onload = function(){
function find(selector, root){
//If no root provided, starts at the top of the document
root = root || document;
//Splits the selector on spaces, grabs the first term, collects the remainder, finds the element matching the first term, and initializes an array to gather the results within
var parts = selector.split(" "), query = parts[0], rest = parts.slice(1).join(" "), elems = root.getElementsByTagName(query), results = [];
for (var i = 0; i < elems.length; i++) {
if (rest) {
//Calls find() recursively until all the selectors are consumed
results = results.concat(find(rest, elems[i]));
}
else {
//Pushes found elements onto results array
restesults.push(elems[i]);
}
}
//Returns list of matched elements
return results;
};
var divs = find("div");
assert(divs.length === 2, "Correct number of divs found.");
var divs = find("div");
assert(divs.length === 2, "Correct number of divs found.");
var divs = find("div", document.body);
assert(divs.length === 2, "Correct number of divs found in body.");
var divs = find("body div");
assert(divs.length === 2, "Correct number of divs found in body.");
var spans = find("div span");
assert(spans.length === 2, "A duplicate span was found.");
};
</script>
在這個實(shí)現(xiàn)中我們做了一個限制,這個引擎只能查找標(biāo)簽名朱沃。這個引擎可以分解為幾個步驟:解析過濾器苞轿,查找元素,過濾逗物,檢索和合并元素搬卒。
我將近距離觀察每個步驟。
15.3.1 解析選擇器 {#15-3-1}
在我們簡單的例子中翎卓,我們只將標(biāo)簽名形式的CSS選擇器轉(zhuǎn)換為字符串?dāng)?shù)組契邀,例如“div span”轉(zhuǎn)換為[“div”, “span”]。
這個簡單的例子使用空白分隔符將字符串隔斷失暴,但是CSS2和CSS3有能力使用屬性或者屬性值來查找元素蹂安,因此可能在選擇器中會多出其它一些空格椭迎。這就使得我們之前使用的方法看起來過于簡化了锐帜。
為了完整的實(shí)現(xiàn)田盈,我們想得到一系列可靠的解析規(guī)則,用來處理那此拋過來的任何表達(dá)式缴阎;這個規(guī)則最可能的就是使用正則表達(dá)式允瞧。下面的例子利用正則表達(dá)式展示非常健壯的解析能力,它可以捕獲選擇器的每個部分并將它們截斷(如果有必要的話用逗號分隔)蛮拔。
Listing 15.6 A regular expression for breaking apart a CSS selector
<script type="text/javascript">
var selector = "div.class > span:not(:first-child) a[href]"
var chunker = /((?:\([^\)]+\)|\[[^\]]+\]|[^ ,\(\[]+)+)(\s*,\s*)?/g;
var parts = [];
//Resets the position of the chunker regexp (start from beginning)
chunker.lastIndex = 0;
//Collects the pieces
while ((m = chunker.exec(selector)) !== null) {
parts.push(m[1]);
//Stops on encountering a comma
if (m[2]) {
extra = RegExp.rightContext;
break;
}
}
assert(parts.length == 4, "Our selector is broken into 4 unique parts.");
assert(parts[0] === "div.class", "div selector");
assert(parts[1] === ">", "child selector");
assert(parts[2] === "span:not(:first-child)", "span selector");
assert(parts[3] === "a[href]", "a selector");
</script>
很明顯述暂,這個選擇器只是拼圖中的一部分。我們需要為每個表達(dá)式添加額外的解析規(guī)則建炫。大多數(shù)選擇器引擎都包含一個正則表達(dá)式與方法的映射畦韭,當(dāng)選擇器中某部分匹配上時,關(guān)聯(lián)的方法就會執(zhí)行肛跌。
研究這些表達(dá)式的細(xì)節(jié)會花費(fèi)很多時間艺配。如果你真的想研究它是如何做的,我們鼓勵你去看jQuery的源代碼或者其它你感興趣的類庫的源代碼衍慎,研究選擇器解析那部分转唉。
下面,我們需要找到正則表達(dá)式匹配的元素稳捆。
15.3.2 查找元素 {#15-3-2}
有許多方法可以找到正確的元素赠法。這些技術(shù)依賴于瀏覽器支持哪些技術(shù)以及哪些是有效的。不過乔夯,還是有許多明顯的方法的砖织。
getElementById()只在HTML文檔的根節(jié)點(diǎn)上是有效的,這方法會在文檔中查找指定id的第一個元素(正是這個原因這個id的元素應(yīng)該只有一個)末荐,因此CSS ID選擇器是有效的侧纯。讓人惱火的是IE和Opera還會將name一樣的第一個元素查找出來。如果我們只希望查找ie鞠评,我們就需要一個額外的驗證步驟來去除那些不需要的元素茂蚓。
如果我們想要找到與指定id匹配的所有元素(這是CSS選擇器的習(xí)慣,即使每個頁面被規(guī)定只有一個指定id的元素)剃幌,我需要遍歷所有元素或者使用document.all[“id”]聋涨。document.all[“id”]將所有匹配的元素以數(shù)組的形式返回,支持此方法的瀏覽器包括IE负乡,Opera牍白,和Safari。
getElementByTagName()方法根據(jù)標(biāo)簽名匹配元素抖棘,另外一種用法就是通過*做為標(biāo)簽名可以查找文檔或者元素中的所有元素茂腥。在處理基于屬性的選擇器(例如:.class或者[attr])的時候這種方法是非常有用的狸涌。
使用*的時候有一個警告,IE返回的結(jié)果中包括注釋節(jié)點(diǎn)(因為標(biāo)簽的節(jié)點(diǎn)名是最岗!帕胆,因此會被返回)。所以需要過濾注釋節(jié)點(diǎn)般渡。
getElementsByName()是一個非常容易實(shí)現(xiàn)的方法懒豹,它的目的是查找指定名稱的所有元素(例如<input>標(biāo)簽和那些擁有name屬性的標(biāo)簽)。實(shí)現(xiàn)這個[name=name]選擇器是非常有用的驯用。
getElementByClassName()是一個新的HTML5方法脸秽。它是基于class屬性來查找元素的。這個方法巨大地提升了類選擇器的速度蝴乔。
雖然有許多技術(shù)可以被用來實(shí)現(xiàn)選擇器记餐,但是上述我們所說的方法是查找元素時最主要的方法。
使用這些方法的結(jié)果薇正,我們移步到過濾部分片酝。
15.3.3 過濾集合 {#15-3-3}
一個CSS表達(dá)式通常包含許多獨(dú)立的部分。例如div.class[id]包含三個部分:它會查找那些類名為class并且擁有一個屬性名為id的div元素铝穷。
第一步是標(biāo)識根選擇器钠怯。例如,我們看到使用了div曙聂,所以我們使用getElementsByTagName()方法遍歷頁面上所有的div元素晦炊。然后,我們過濾出這些結(jié)果中只包含指定類名和擁有id屬性的元素宁脊。
這個過濾的功能常見于所有的選擇器中断国。過濾器中的內(nèi)容最主要的是處理元素的屬性和根據(jù)元素的兄弟節(jié)點(diǎn)和其它關(guān)系確定位置。
- 屬性過濾——這個方法的實(shí)現(xiàn)是訪問DOM的屬性(一般使用getAttribute方法)和驗證它們的值榆苞。Class過濾是這個方法的一種形式(訪問className屬性并檢查它的值)稳衬。
- 位置過濾——例如選擇器:nth-child(event)或者:last-child,它們是使用在父元素上的多種方法的結(jié)合體坐漏。在那些支持它的瀏覽器中使用children(IE薄疚,Safari,Chrome赊琳,Opera街夭,和Firefox3.1),children中包含所有的child元素躏筏。所有的瀏覽器都包含childNodes板丽,它包含子元素列表,其中也包含text節(jié)點(diǎn)和注釋趁尼。使用這兩種方法可以做到各種形式的元素位置過濾埃碱。
構(gòu)建一個過濾器方法有兩個目的:我們可以為用戶提供一個簡單的方法用來測試它們的元素猖辫,并且我們可以快速的檢查一個元素是否與指定選擇器匹配。
現(xiàn)在讓我們關(guān)注細(xì)化結(jié)果的工具砚殿。
15.3.4 遍歷與合并
選擇器引擎需要有能力去遍歷(查找后裔元素)和合并結(jié)果啃憎。但是像我們listing那樣,我們的初始實(shí)現(xiàn)還差得很遠(yuǎn)瓮具。我們最終的結(jié)果是得到兩個<span>元素荧飞,而不是一個。我們需要引入一個額外的檢查來確保返回的數(shù)組中有唯一的結(jié)果名党。大多數(shù)從上到下的選擇器引擎都擁有保持一致性的工具。
不走運(yùn)的是挠轴,沒有一個簡單的方式可以確保DOM元素的一致性传睹,所以我們需要自己來實(shí)現(xiàn)它。我們會遍歷這些元素并為它們分配臨時的標(biāo)識岸晦,因此我們可以驗證我們是否遇到過它們欧啤。
Listing 15.7 Finding the unique elements in an array
//Sets up our willing test subjects.
<div id="test">
<b>Hello</b>, I'm a ninja!
</div>
<div id="test2"></div>
<script type="text/javascript">
(function(){
//Defines the unique() function inside an immediate function to create a closure that will include the run variable but hide it from the outside world.
var run = 0;
//Accepts an array of elements and returns a new array containing only unique elements from the original array.
this.unique = function(array) {
var ret = [];
//Keeps track of which run we’re on. By incrementing this value each time the function is called, a unique identifier value will be used for testing for uniqueness.
run++;
for (var i = 0, length = array.length; i < length; i++) {
var elem = array[i];
//Runs through the original array, copying elements that we haven’t “seen” yet, and marking them so that we’ll know whether we’ve “seen” them or not.
if (elem.uniqueID !== run) {
elem.uniqueID = run;
ret.push(array[i]);
}
}
//Returns the resulting array, containing only references to unique elements.
return ret;
};
})();
window.onload = function(){
//Tests it! The first test shouldn’t result in any removals (as there are no duplicates passed), but the second should collapse down to a single element.
var divs = unique(document.getElementsByTagName("div"));
assert(divs.length === 2, "No duplicates removed.");
var body = unique([document.body, document.body]);
assert(body.length === 1, "body duplicate removed.");
};
</script>
unique()方法在檢驗數(shù)組中的元素時為它們添加了額外的屬性,標(biāo)記它們是被看過的启上。遍歷完成后邢隧,只有唯一的元素被復(fù)制到結(jié)果的數(shù)組當(dāng)中。在所有的類庫中都會發(fā)現(xiàn)這種驗證方法冈在。
想要了解屬性綁定那部分可以回顧13章中涉及到事件的那部分倒慧。
上面我們考慮的都是從上到下的方法。讓我們快速粗略地了解一下另外一種方式包券。
15.3.5 從下到上的選擇器引擎 {#15-3-5}
如果你不關(guān)心元素唯一性的話纫谅,從下到上的選擇器引擎也是一個選擇。從下到上的選擇器引擎與從上到下是相反的溅固。
如果選擇器是div span付秕,選擇器引擎會先找出所有的<span>元素,然后尋找每個元素的<div>祖先元素侍郭。這種選擇器引擎在大多數(shù)瀏覽器引擎中都會找到询吴。
這種引擎并不像從上到下的方法那樣流行。雖然對于簡單的選擇器(子選擇器)它運(yùn)行起來非常良好亮元,遍歷祖先開銷非常大猛计。但是它的簡單性值得我的丑爹產(chǎn)權(quán)衡。
這個引擎的構(gòu)造非常簡單苹粟。在CSS選擇器中我們查找最后的表達(dá)式有滑,然后遍歷合適的元素(就像從上到下引擎,但是使用的是最后的表達(dá)式)嵌削。從此開始毛好,所有的操作都是一系列的過濾操作和一些在過程中刪除元素的操作(看下面的代碼)望艺。
Listing 15.8 A simple bottom-up selector engine
<div>
<div>
<span>Span</span>
</div>
</div>
<script type="text/javascript">
window.onload = function(){
function find(selector, root){
root = root || document;
var parts = selector.split(" "),
query = parts[parts.length - 1],
rest = parts.slice(0,-1).join(""),
elems = root.getElementsByTagName(query),
results = [];
for (var i = 0; i < elems.length; i++) {
if (rest) {
var parent = elems[i].parentNode;
while (parent && parent.nodeName != rest) {
parent = parent.parentNode;
}
if (parent) {
results.push(elems[i]);
}
} else {
results.push(elems[i]);
}
}
return results;
};
var divs = find("div");
assert(divs.length === 2, "Correct number of divs found.");
var divs = find("div", document.body);
assert(divs.length === 2, "Correct number of divs found in body.");
var divs = find("body div");
assert(divs.length === 2, "Correct number of divs found in body.");
var spans = find("div span");
assert(spans.length === 1, "No duplicate span was found.");
};
</script>
Listing 15.8 展示了從下到上引擎的構(gòu)造。注意它只向上查找了一級肌访。如果想要做到更深的級別就需要跟蹤當(dāng)前的級別找默。這里會返回兩種形式的數(shù)組:一些沒有被設(shè)置為undefinded的元素的數(shù)組,因為它們不匹配結(jié)果吼驶。另外就是與可以正確匹配祖先元素的元素數(shù)組惩激。
之前提到過,額外的驗證祖先元素的工作會導(dǎo)致?lián)p失擴(kuò)展性蟹演,但是它不使用保證一致性的方法就可以產(chǎn)生不重復(fù)的結(jié)果风钻,這就是它的優(yōu)點(diǎn)。
15.4 總結(jié) {#15-4}
基于JavaScript的CSS選擇器引擎難以置信的強(qiáng)大酒请。它們可以讓我使用平凡的選擇器語法輕松地定位頁面上的DOM元素骡技。實(shí)現(xiàn)一個完整的選擇器引擎需要考慮許多細(xì)節(jié),但是隨著瀏覽器的提升這些條件會被修復(fù)的羞反,我們并不缺少工具布朦。
本章我們學(xué)習(xí)到的東西:
- 現(xiàn)在主流的瀏覽器實(shí)現(xiàn)了W3C規(guī)定元素選擇的 API,但是它們?nèi)杂泻荛L的路要走昼窗。
- 如果沒有性能的問題是趴,自己創(chuàng)建選擇器引擎對我們是有好處的。
- 為了創(chuàng)建一個選擇器引擎澄惊,我們可以
- 利用W3C APIs
- 使用XPath
- 為了更好的性能自己轉(zhuǎn)換DOM
- 從上到下的方法是最流行的唆途,但是它要求一些清理工作,例如確保元素的一致性缤削。
- 從下到上避免了這些操作窘哈,但是它存在性能和擴(kuò)展性方面的問題。
使用瀏覽器實(shí)現(xiàn)的W3C API亭敢,對選擇器實(shí)現(xiàn)的擔(dān)心很快就會成為過去滚婉。對于許多開發(fā)人員來說,這一個可能不會太快到來帅刀。