天天看點

jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢

為什麼Sizzle很高效?

首先,從處理流程上了解,它總是先使用最高效的原生方法來做處理

HTML文檔一共有這麼四個API:

getElementById 上下文隻能是HTML文檔       

浏覽器支援情況:IE 6+, Firefox 3+, Safari 3+, Chrome 4+, and Opera 10+;

getElementsByName,上下文隻能是HTML文檔      

浏覽器支援情況:IE 6+, Firefox 3+, Safari 3+,Chrome 4+, and Opera 10+;

getElementsByClassName      

浏覽器支援情況:IE 9+, Firefox 3+, Safari4+, Chrome 4+, and Opera 10+;

getElementsByTagName      

上下文可以是HTML文檔,XML文檔及元素節點。

進階API:

浏覽器支援情況:IE 8+, Firefox 3.5+, Safari 3+, Chrome 4+, and Opera 10+;

querySelector 将傳回比對到的第一個元素,如果沒有比對的元素則傳回 Null
querySelectorAll 傳回一個包含比對到的元素的數組,如果沒有比對的元素則傳回的數組為空      

浏覽器内置的css選擇符查詢元素方法,比getElementsByTagName和getElementsByClassName效率要高很多

前者接收一個CSS選擇器字元串參數并傳回一個NodeList類數組對象而不是傳回HTML集合,後者隻傳回符合查詢條件的第一個節點。很遺憾IE6、7不支援這兩個API。

性能測試參考:http://jsperf.com/queryselectorall2

總的來說還是 

document.getElementById 速度最快

Sizzle原理:

  1. 浏覽器原生支援的方法,效率肯定比Sizzle自己js寫的方法要高,優先使用也能保證Sizzle更高的工作效率,在不支援querySelectorAll方法的情況下,Sizzle也是優先判斷是不是可以直接使用getElementById、getElementsByTag、getElementsByClassName等方法解決問題。
  2. 相對複雜的情況,Sizzle總是選擇先盡可能利用原生方法來查詢選擇來縮小待選範圍,然後才會利用前面介紹的“編譯原理”來對待選範圍的元素逐個比對篩選。進入到“編譯”這個環節的工作流程有些複雜,效率相比前面的方法肯定會稍低一些,但Sizzle在努力盡量少用這些方法,同時也努力讓給這些方法處理的結果集盡量小和簡單,以便獲得更高的效率。
  3. 即便進入到這個“編譯”的流程,Sizzle還做了我們前面為了優先解釋清楚流程而暫時忽略、沒有介紹的緩存機制。Sizzle.compile是“編譯”入口,也就是它會調用第三個核心方法superMatcher,compile方法将根據selector生成的比對函數緩存起來了。還不止如此,tokenize方法,它其實也将根據selector做的分詞結果緩存起來了。也就是說,當我們執行過一次Sizzle (selector)方法以後,下次再直接調用Sizzle (selector)方法,它内部最耗性能的“編譯”過程不會再耗太多性能了,直接取之前緩存的方法就可以了。我在想所謂“編譯”的最大好處之一可能也就是便于緩存,所謂“編譯”在這裡可能也就可以了解成是生成預處理的函數存儲起來備用。

整個過程在sizzle源碼分解都有詳細的流程分解,還有緩存機制,XML,僞選擇器,後期在補上

如何打造高效的選擇器?

jQuery選擇器使用頻率清單

jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢
jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢

正确使用選擇器引擎對于提高頁面性能起了至關重要的作用。使用合适的選擇器表達式可以提高性能、增強語義并簡化邏輯。在傳統用法中,最常用的簡單選擇器包括ID選擇器、Class選擇器和類型标簽選擇器。其中ID選擇器是速度最快的,這主要是因為它使用JavaScript的内置函數getElementById();其次是類型選擇器,因為它使用JavaScript的内置函數getElementsByTag();速度最慢的是Class選擇器,其需要通過解析 HTML文檔樹,并且需要在浏覽器核心外遞歸,這種遞歸周遊是無法被優化的。

Class選擇器在文檔中使用頻率靠前,這無疑會增加系統的負擔,因為每使用一次Class選擇器,整個文檔就會被解析一遍,并周遊每個節點。

基本的幾個選擇器的測試

性能測試網址

JSPerf (http://jsperf.com/)

Dromaeo (http://dromaeo.com/)

測試一

<div id="text">
     <input id='aaron'  class="aaron"  type="checkbox" name="readme" value="Submit" 
</div>      
jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢
jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢

毋庸置疑 id是最快的, 因為節點較少 是以來看出class與tag的差別

測試二

jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢
<div id = "demo" > 
  <ul> 
    <li> </li>
    <li></li > 
    <li> </li>
    <li></li > 
 </ul>
</div >      
jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢
jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢
jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢

通過對sizzle分析得知都選擇器是從右向左比對, ("#demo li:nth-child(1)") 這句将先比對所有 li元素,在比對#demo("#demo").find("li:nth-child(1)") 而這裡則先比對#demo,再從中找比對li,比對範圍縮短,效率明顯提升

測試三

jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢
<div id="text">
  <p>
     <input type="text" />
  </p>
  <div class="aaron">
     <input type="checkbox" name="readme" value="Submit" />
     <p>Sizzle</p>
  </div>
</div>      
jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢
jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢
jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢

為什麼差距這麼大?

因為采用了CSS的屬性表達式,是以Sizzle用.querySelectorAll()來查找元素

$(‘input:text’),采用了jQuery自定義的選擇器表達式:text,.querySelectorAll()方法無法解析

是以,在jqury中,一些選擇器表達式普遍快于另外一些選擇器表達式,把選擇器中的僞類移到相應的方法中可以加速查找頁面文檔dom元素的時間

為了簡單起見,我們把jQuery中用.getElementById (),.getElementsByTagName(),.getElementsByClassName() 這3個方法的結合來查找元素稱為:循環和檢驗(loop and test)過程。

測試總結:

圖形測試很簡單,每秒執行的操作,是以,數值越高,執行效率越好,代表執行時間越短,性能越好

在現代浏覽器中,(Chrome 12, Firefox4, and Safari 5,IE 8+) ,CSS選擇器表達式底層采用.querySelectorAll()方法,很好的實作了優勢,平均而言,大概是自定義選擇器表達式性能表現的2倍。但是,在ie7中,這兩個選擇器的性能表現差不多,這是因為在ie7環境下,Sizzle都采用了循環和檢驗(loop and test)過程累找到相應的元素,(因為ie7不支援.querySelectorAll()方法。),是以在編寫jQuery的選擇器函數進行事件注冊時,要特别注意,可能你的代碼在ie8以上執行正确,但在ie7中,$()函數傳回的object.length将是0

選擇器性能優化建議

http://learn.jquery.com/performance/optimize-selectors/

第一,多用ID選擇器 , 總是從#id選擇器來繼承

多用ID選擇器,這是一個明智的選擇。即使添加"在"ID選擇器,也可以從父級元素中添加一個ID選擇器,這樣就會縮短節點通路的路程。

這是jQuery選擇器的一條黃金法則。jQuery選擇一個元素最快的方法就是用ID來選擇了

$('#content').hide();      

或者從ID選擇器繼承來選擇多個元素

$('#content p').hide();      

再如

$("#container").find("div.robotarm");      

效率更高,那是因為$("#container")是不需要經過Sizzle選擇器引擎處理的,jquery對僅含id選擇器的處理方式是直接使用了浏覽器的内置函數document.getElementById(),是以其效率是非常之高的。

特征性

使一個選擇器的右邊更具有特征,相對而言,選擇器的左邊可以少一些特征性。

// unoptimized  優化前
$( "div.data .gonzalez" );
  
 // optimized     優化後
$( ".data td.gonzalez" );      

  再選擇器的右邊盡可能使用"tag.class"類型的選擇符,在選擇器的左邊直接使用标簽選擇符或類選擇符即可。

  (類似于css選擇器,其比對算法是從右至左的)

避免過度的限制
$(".data table.attendees td.gonzalez");
  
// better: drop the middle if possible   盡可能移除掉中間的
 $(".data td.gonzalez");      

一個更為“扁平”的DOM結構,會使得選擇器引擎在尋找元素時經過的層次數更少,是以這樣也是有利于提高選擇器的性能的。

避免使用全局的選擇器

一個會被在多處地方成功比對的選擇器可能會消耗更多的性能

jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢
$(".buttons > *");  // extremely expensive
 $(".buttons").children();  // much better
  
 $(".gender :radio");  // implied universal selection
 $(".gender *:radio"); // same thing, explicit now
 $(".gender input:radio"); // much better      
jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢

第二,少直接使用Class選擇器。

可以使用複合選擇器,例如使用tag.class代替.class。文檔的标簽是有限的,但是類可以拓展标簽的語義,那麼大部分情況下,使用同一個類的标簽也是相同的。

當然,應該摒除表達式中的備援部分,對于不必要的複合表達式就應該進行簡化。例如,對于#id2 #id1 或者 tag#id1表達式,不妨直接使用#id1即可,因為ID選擇器是惟一的,執行速度最快。使用複合選擇器,相反會增加負擔。      

在class前面使用tag

jQuery中第二快的選擇器就是tag選擇器(如$(‘head’)),因為它和直接來自于原生的Javascript方法getElementByTagName()。是以最好總是用tag來修飾class(并且不要忘了就近的ID)

var receiveNewsletter = $('#nslForm input.on');      

jQuery中class選擇器是最慢的,因為在IE浏覽器下它會周遊所有的DOM節點。盡量避免使用class選擇器。也不要用tag來修飾ID。下面的例子會周遊所有的div元素來查找id為’content’的那個節點:

var content = $('div#content'); // 非常慢,不要使用      

用ID來修飾ID也是畫蛇添足:

var traffic_light = $('#content #traffic_light'); // 非常慢,不要使用      

第三,多用父子關系,少用嵌套關系。

例如,使用parent>child代替parent child。因為">"是child選擇器,隻從子節點裡比對,不遞歸。而" "是後代選擇器,遞歸比對所有子節點及子節點的子節點,即後代節點。      

下面六個選擇器,都是從父元素中選擇子元素。你知道哪個速度最快,哪個速度最慢嗎?

jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢
$('.child', $parent)
$parent.find('.child')
$parent.children('.child')
$('#parent > .child')
$('#parent .child')
$('.child', $('#parent'))      
jQuery 2.0.3 源碼分析Sizzle引擎 - 高效查詢

1. 給定一個DOM對象,然後從中選擇一個子元素。jQuery會自動把這條語句轉成$.parent.find('child'),這會導緻一定的性能損失。它比最快的形式慢了5%-10%。

$('.child', $parent)      

3. 這條是最快的語句。.find()方法會調用浏覽器的原生方法(getElementById,getElementByName,getElementByTagName等等),是以速度較快。

$parent.find('.child')      

3. 這條語句在jQuery内部,會使用$.sibling()和javascript的nextSibling()方法,一個個周遊節點。它比最快的形式大約慢50%

parent.children('.child'):      

4. jQuery内部使用Sizzle引擎,處理各種選擇器。Sizzle引擎的選擇順序是從右到左,是以這條語句是先選.child,然後再一個個過濾出父元素#parent,這導緻它比最快的形式大約慢70%。

$('#parent > .child'):      

5 這條語句與上一條是同樣的情況。但是,上一條隻選擇直接的子元素,這一條可以于選擇多級子元素,是以它的速度更慢,大概比最快的形式慢了77%。

$('#parent .child'):      

6 jQuery内部會将這條語句轉成$('#parent').find('.child'),比最快的形式慢了23%。

$('.child', $('#parent')):      

是以,最佳選擇是 parent.find(′.child′)。而且,由于 parent往往在前面的操作已經生成,jQuery會進行緩存,是以進一步加快了執行速度。

第四,緩存jQuery對象。

如果選出結果不發生變化的話,不妨緩存jQuery對象,這樣就可以提高系統性能。養成緩存jQuery對象的習慣可以讓你在不經意間就能夠完成主要的性能優化。      
下面的用法是低效的。
for (i = 0 ; i < 10000; i ++ ) ... {   
      var a= $( ' .aaron' );   
     a.append(i);   
}      
而使用下面的方法先緩存jQuery對象,則執行效率就會大大提高。
var a= $( ' .aaron' );   
for (i = 0 ; i < 10000 ; i ++ ) ... {   
     a.append(i);   
}      

通過鍊式調用,采用find(),end(),children(),has,filter()等方法,來過濾結果集,減少$()查找方法調用,提升性能

$('#news').find('tr.alt').removeClass('alt').end().find('tbody').each(function() {
        $(this).children(':visible').has('td').filter(':group(3)').addClass('alt');
   });      

修改下,緩存結果集示例:

var $news = $('#news');
 $news.find('tr.alt').removeClass('alt');
 $news.find('tbody').each(function() {
         $(this).children(':visible').has('td').filter(':group(3)').addClass('alt');
  });      

通過聲明 news變量緩存 (‘#news’)結果集,進而提升後面結果集對象調用方法的性能。

總的來說,做為一個常見的規則,我們應該盡量使用符合CSS文法規範的CSS選擇器表達式,以此來避免使用jQuery自定義的選擇器表達式

在jQuery選擇器性能測試方面,可以采用http://jsperf.com/這個線上工具來檢驗哪種編寫方法對性能的改進影響更大

跟jQuery選擇器有關的性能問題是盡量采用鍊式調用來操作和緩存選擇器結果集。

因為每一個$()的調用都會導緻一次新的查找,是以,采用鍊式調用和設定變量緩存結果集,減少查找,提升性能。