iT邦幫忙

6

jQuery 選擇器之虛擬選擇器 (pseudo selector)

介紹一些虛擬選擇器,並且說明如何擴充虛擬選擇器。 :)
基本上 selector 嚴格分類來講有相當多種, slibing / parent ...etc

可以看這裡有分類
http://api.jquery.com/category/selectors/

CSS 提供給我們的那些相信大家都不陌生,ID(#)/Tag/class(.)/attr([])/child(>) ...etc

其中我要介紹的是一些算是比較不屬於 css3 規範的異類 selector ,
大家可能常用(至少我常用),但是不見得知道他的運作模式,

這裡我們就是要來告訴你他是什麼。:)

jQuery 的 selector 其實暗藏很多我們不知道的玄機,我會盡量再多介紹一些。


ok 不賣關子,我們開始談主題。

大家有沒有用過 pseudo selector ,像是這個範例?

<div class="test"></div>
<div class="test" style="display:none"></div>
<div class="test" style="display:none"></div>

<script>
   alert($(".test:visible").length);
</script>

Runnable sample : http://jsfiddle.net/GGRD6/

":visible" selector,我相信大家都知道這不是 css 規格,但是它會動。:)

這種冒號開頭的選擇器,jQuery 稱之為虛擬選擇器。
(其實還再細分出一種是 pos selector
如 :eq / :lt / :odd ..etc ,剩下的都稱為 pseudo selector,
但這裡我先統稱為 pseudo selector 以便說明,原理是相近的。)

這時候我們就會需要來看看 jQuery 官方文件:
http://api.jquery.com/visible-selector/

Because :visible is a jQuery extension and not part of the CSS specification, queries using :visible cannot take advantage of the performance boost provided by the native DOM querySelectorAll() method. To achieve the best performance when using :visible to select elements, first select the elements using a pure CSS selector, then use .filter(":visible").

(不負責翻譯)

因為 :visible 是一個 jQuery extension 而非 CSS 規格,使用 :visible 查詢時,
將無法利用到原生的 querySelectorAll() 函式,會比較慢。
要獲得最好效能,建議是先使用純 css 查詢再使用 filter 進行過濾。

重要的是這裡印證虛擬選擇器大多都不是 css 官方規格,
這裡他同時說兩件我們需要知道的事情,但效能部份我們先不管,
我們文末會回頭再談,我們先介紹虛擬選擇器的原理。


為了說明,我想 :visible 一個例子是不夠的,我再舉另一個例子 ":contains()" 好了,
這也是相當常見的例子,尋找指定元素內有包含特定字串的。

<div>
    <div class="test" style="display:none">I am a div </div>
    <div class="test" style="display:none"> I am also div  </div>
    <div class="test" style="display:none">I am still a div  </div>
</div>
​
<script>
$(document.body).append(".test:contains('div') #=> "+ $(".test:contains('div')").length +"<br />");
$(document.body).append(".test:contains('still') #=> "+ $(".test:contains('still')").length  +"<br />");
​</script>

將會輸出
.test:contains('div') #=> 3
.test:contains('still') #=> 1

runnable sample http://jsfiddle.net/GGRD6/1/

官方文件 http://api.jquery.com/contains-selector/


當然還有 :not 這個一定是不可少的。

<div>
    <div class="test test1" style="display:none">I am a div </div>
    <div class="test test2" style="display:none"> I am also div  </div>
    <div class="test test3" style="display:none">I am still a div  </div>
</div>
<script>
$(document.body).append(".test:not('.test1') #=> "+ $(".test:not('.test1')").length +"<br />");
$(document.body).append(".test:not('.test1,.test2') #=> "+ $(".test:not('.test1,.test2')").length  +"<br />");
​</script>
​

輸出
.test:not('.test1') #=> 2
.test:not('.test1,.test2') #=> 1

官方文件 http://api.jquery.com/not-selector/


你一定很好奇為什麼我要特別介紹這個,我可以一個一個作 selector 的介紹,
為什麼要介紹「pseudo selector 」,他有什麼好介紹的?

除了效能議題以外,其實我更重視的是他實作的邏輯,
我曾經很好奇 jQuery 是怎麼實作的而翻過很多次他的原始碼,
而其中這一個是我特別有印象的。:)

他有趣的地方在於一是實作跟虛擬選擇器的對應,另外就是他的可擴充性。

以我們 JS developer 來講,
碰到 selelector 這種帶著規則的「字串」東西其實蠻沒轍的,
他又不是 call 一個純 js function ,你如果想 trace 也要多繞好幾圈才看的懂。

而 pseudo selector 的條件定義呢,
就由 jQuery.expr.filters 跟 jQuery.expr.setFilters (for pos selectors) 掌管,
所以你可以翻原始碼,也可以用這種方式找出所有虛擬選擇器:

var filters = jQuery.expr.filters;
for(var i in filters  ){
    $(document.body).append(i+" #=> "+filters [i].toString()+"<br /><br />");   
}
$(document.body).append("<div style='color:red;'>setFilters </div><br />");   
filters = jQuery.expr.setFilters;
for(var i in filters  ){
    $(document.body).append(i+" #=> "+filters [i].toString()+"<br /><br />");   
}

因為 details 有點多,想看細節的實作的可以移步 XD
http://jsfiddle.net/yAAqD/

翻翻 jQuery 1.7.2 的 4565 ~ 4686 行程式碼~(這只是其中部份。)

你會發現他基本上流程是這麼作:

1.先檢查是不是 pos selector ,是的話就跑 jQuery.expr.setFilters 出來作 filter。
(:nth , :eq, :gt, :lt, :first, :last, :even, :odd )

2.如果不是,就找 jQuery.expr.filters

如果後面還有跟其他 selector ,會再套疊後續的 operator 去做查詢。


你可能會好奇,如果我給 $(".test:myfilter") 會發生什麼事情,答案是會噴 error:
Syntax error, unrecognized expression: myfilter


所以理論上要擴充一個新的 filter ,直接擴充 jQuery.expr.filters 就可以了。
(如果你要擴充 set selector ,要改比較多東西,有興趣再問吧XD)

舉個例子,我今天比較機車,我想留 id 裡面有包含 tony 的元素,我可以這樣實作。

jQuery.expr.filters.isTony = function(elem){
      return elem.id && elem.id.indexOf("tony") != -1;
}

//query with the pseudo selector 
//$("div:isTony")

Sample http://jsfiddle.net/LMBgN/


至於效能,基本上就如官網文件所說。

基本上 "查詢" ($(selector) 或 $parent.find(selector)) ,
能不用虛擬 selector 就不該用;

而"過濾" ($set.filter(selector) or $set.is(selector )) 就很適合用。:)


文末的文末,補個題外話,我一直很訝異的是,
jQuery mobile 是用這麼髒的方式,來實作他們需要的內部用 pseudo selector。:P

有時候看看別人的實作,想想背後的理由,也是蠻有意思的。:P

	// Monkey-patching Sizzle to filter the :jqmData selector
	var oldFind = $.find,
		jqmDataRE = /:jqmData\(([^)]*)\)/g;

	$.find = function( selector, context, ret, extra ) {
		selector = selector.replace( jqmDataRE, "[data-" + ( $.mobile.ns || "" ) + "$1]" );

		return oldFind.call( this, selector, context, ret, extra );
	};

這篇因為想睡了講的比較亂,有興趣/疑問歡迎發問。:)


尚未有邦友留言

立即登入留言