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原理:
- 浏览器原生支持的方法,效率肯定比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