第15章 CSS選擇引擎

第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ā)為我們提供基本的幫助。如果你在捉摸我們要如何做疗认,主要遵循以下模式:

  1. 查找DOM元素完残。
  2. 對它們做一些事情。

除了新的選擇器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ā)人員來說,這一個可能不會太快到來帅刀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末让腹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子扣溺,更是在濱河造成了極大的恐慌骇窍,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锥余,死亡現(xiàn)場離奇詭異腹纳,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進(jìn)店門嘲恍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來足画,“玉大人,你說我怎么就攤上這事佃牛⊙痛牵” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵俘侠,是天一觀的道長象缀。 經(jīng)常有香客問我,道長爷速,這世上最難降的妖魔是什么央星? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮遍希,結(jié)果婚禮上等曼,老公的妹妹穿的比我還像新娘。我一直安慰自己凿蒜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布胁黑。 她就那樣靜靜地躺著废封,像睡著了一般。 火紅的嫁衣襯著肌膚如雪丧蘸。 梳的紋絲不亂的頭發(fā)上漂洋,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機(jī)與錄音力喷,去河邊找鬼刽漂。 笑死,一個胖子當(dāng)著我的面吹牛弟孟,可吹牛的內(nèi)容都是我干的贝咙。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼拂募,長吁一口氣:“原來是場噩夢啊……” “哼庭猩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起陈症,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤蔼水,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后录肯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體趴腋,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了优炬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颁井。...
    茶點(diǎn)故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖穿剖,靈堂內(nèi)的尸體忽然破棺而出蚤蔓,到底是詐尸還是另有隱情,我是刑警寧澤糊余,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布秀又,位于F島的核電站,受9級特大地震影響贬芥,放射性物質(zhì)發(fā)生泄漏吐辙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一蘸劈、第九天 我趴在偏房一處隱蔽的房頂上張望昏苏。 院中可真熱鬧,春花似錦威沫、人聲如沸贤惯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽孵构。三九已至,卻和暖如春烟很,著一層夾襖步出監(jiān)牢的瞬間颈墅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工雾袱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恤筛,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓芹橡,卻偏偏與公主長得像毒坛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子僻族,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容

  • 問答題47 /72 常見瀏覽器兼容性問題與解決方案粘驰? 參考答案 (1)瀏覽器兼容問題一:不同瀏覽器的標(biāo)簽?zāi)J(rèn)的外補(bǔ)...
    _Yfling閱讀 13,737評論 1 92
  • 山里的童年 組詩 爺爺與大山 住在青山深處的土房子里蝌数, 門口是郁郁蔥蔥的老梧桐樹, 樹上都是些喝醉了的知了度秘, 胡...
    秋音南客閱讀 657評論 1 7
  • 去年暑假回老家考駕照顶伞,因為車少人多饵撑,基本一整天都待在駕校里面排隊練車,久而久之唆貌,便認(rèn)識了一些人滑潘。這其中就有一個女孩...
    茉與默閱讀 1,455評論 2 0
  • 午加餐:面包晚水果:石榴 參考目標(biāo): 1份肉2份豆制品3份“新鮮”水果4份谷物/薯5份蔬菜,深綠色葉菜最好6杯水 ...
    靜趣_兒童心理師閱讀 280評論 0 0
  • 叮鈴鈴粹舵,叮鈴鈴…… 期待了40分鐘的鈴聲響起來了。 像往常一樣骂倘,回過頭眼滤,和你說話,和你玩鬧……快樂就是這么簡單历涝。 ...
    Ykay張閱讀 132評論 0 0