為什么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原理:
- 瀏覽器原生支持的方法,效率肯定比Sizzle自己js寫的方法要高,優先使用也能保證Sizzle更高的工作效率,在不支持querySelectorAll方法的情況下,Sizzle也是優先判斷是不是可以直接使用getElementById、getElementsByTag、getElementsByClassName等方法解決問題。
- 相對復雜的情況,Sizzle總是選擇先盡可能利用原生方法來查詢選擇來縮小待選范圍,然后才會利用前面介紹的“編譯原理”來對待選范圍的元素逐個匹配篩選。進入到“編譯”這個環節的工作流程有些復雜,效率相比前面的方法肯定會稍低一些,但Sizzle在努力盡量少用這些方法,同時也努力讓給這些方法處理的結果集盡量小和簡單,以便獲得更高的效率。
- 即便進入到這個“編譯”的流程,Sizzle還做了我們前面為了優先解釋清楚流程而暫時忽略、沒有介紹的緩存機制。Sizzle.compile是“編譯”入口,也就是它會調用第三個核心方法superMatcher,compile方法將根據selector生成的匹配函數緩存起來了。還不止如此,tokenize方法,它其實也將根據selector做的分詞結果緩存起來了。也就是說,當我們執行過一次Sizzle (selector)方法以后,下次再直接調用Sizzle (selector)方法,它內部最耗性能的“編譯”過程不會再耗太多性能了,直接取之前緩存的方法就可以了。我在想所謂“編譯”的最大好處之一可能也就是便於緩存,所謂“編譯”在這里可能也就可以理解成是生成預處理的函數存儲起來備用。
整個過程在sizzle源碼分解都有詳細的流程分解,還有緩存機制,XML,偽選擇器,后期在補上
如何打造高效的選擇器?
jQuery選擇器使用頻率列表
正確使用選擇器引擎對於提高頁面性能起了至關重要的作用。使用合適的選擇器表達式可以提高性能、增強語義並簡化邏輯。在傳統用法中,最常用的簡單選擇器包括ID選擇器、Class選擇器和類型標簽選擇器。其中ID選擇器是速度最快的,這主要是因為它使用JavaScript的內置函數getElementById();其次是類型選擇器,因為它使用JavaScript的內置函數getElementsByTag();速度最慢的是Class選擇器,其需要通過解析 HTML文檔樹,並且需要在瀏覽器內核外遞歸,這種遞歸遍歷是無法被優化的。
Class選擇器在文檔中使用頻率靠前,這無疑會增加系統的負擔,因為每使用一次Class選擇器,整個文檔就會被解析一遍,並遍歷每個節點。
基本的幾個選擇器的測試
性能測試網址
Dromaeo (http://dromaeo.com/)
測試一
<div id="text"> <input id='aaron' class="aaron" type="checkbox" name="readme" value="Submit" </div>
毋庸置疑 id是最快的, 因為節點較少 所以來看出class與tag的區別
測試二
<div id = "demo" > <ul> <li> </li> <li></li > <li> </li> <li></li > </ul> </div >
通過對sizzle分析得知都選擇器是從右向左匹配, $("#demo li:nth-child(1)") 這句將先匹配所有 li元素,在匹配#demo $("#demo").find("li:nth-child(1)") 而這里則先匹配#demo,再從中找匹配li,匹配范圍縮短,效率明顯提升
測試三
<div id="text"> <p> <input type="text" /> </p> <div class="aaron"> <input type="checkbox" name="readme" value="Submit" /> <p>Sizzle</p> </div> </div>
為什么差距這么大?
因為采用了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結構,會使得選擇器引擎在尋找元素時經過的層次數更少,因此這樣也是有利於提高選擇器的性能的。
避免使用全局的選擇器
一個會被在多處地方成功匹配的選擇器可能會消耗更多的性能
$(".buttons > *"); // extremely expensive $(".buttons").children(); // much better $(".gender :radio"); // implied universal selection $(".gender *:radio"); // same thing, explicit now $(".gender input:radio"); // much better
第二,少直接使用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選擇器,只從子節點里匹配,不遞歸。而" "是后代選擇器,遞歸匹配所有子節點及子節點的子節點,即后代節點。
下面六個選擇器,都是從父元素中選擇子元素。你知道哪個速度最快,哪個速度最慢嗎?
$('.child', $parent) $parent.find('.child') $parent.children('.child') $('#parent > .child') $('#parent .child') $('.child', $('#parent'))
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選擇器有關的性能問題是盡量采用鏈式調用來操作和緩存選擇器結果集。
因為每一個$()的調用都會導致一次新的查找,所以,采用鏈式調用和設置變量緩存結果集,減少查找,提升性能。
參考網址
http://www.artzstudio.com/2009/04/jquery-performance-rules/
http://zhangqi.im/webdevelopment/jquery-performance-optimization-guidelines.html