9.2 定制与优化选择符

将我们看到的那么多技术放到一起,就像是配备了一个工具箱。工具箱中的工具能够帮我们在页面中查找想要操作的任何元素。但我们要了解的还不止这些。怎么才能更加有效地查找元素也是必须关心的一个问题。所谓有效,可能会反映在几个方面。比如代码容易读、容易写,再比如代码在浏览器中运行的速度更快。

9.2.1 编写定制的选择符插件

提高代码可读性的一种方式是把代码片段封装为可以重用的组件。我们之所以创建函数就是这个目的。第8章刚刚讨论了这种思想的延伸,那就是以创建jQuery插件的方式为jQuery对象添加方法。不过,插件并不局限于为jQuery对象添加方法,还可以让我们自定义选择符表达式,例如第7章介绍的Cycle插件中的:paused选择符。

最容易添加的选择符是伪类,也就是以冒号开头的选择符表达式,比如 :checked:nth-child()。为了演示创建选择符表达式的过程,我们会讨论构建一个名为:group()的伪类。这个新选择符将用于封装代码清单9-6中的那些查找表格行并为它们添加条纹效果的代码。

在使用选择符表达式查找元素的时候,jQuery会在一个内部的对象expr中取得JavaScript代码。这个对象中的值与我们传入到.filter().not()中的筛选函数非常相似,当且仅当取得的函数返回true的情况下,才会让每个元素包含在结果集中。使用$.extend()函数可以为这个对象添加新的表达式,参见代码清单9-8。

代码清单9-8

  1. (function($) {
  2. $.extend($.expr[':'], {
  3. group: function(element, index, matches, set) {
  4. var num = parseInt(matches[3], 10);
  5. if (isNaN(num)) {
  6. return false;
  7. }
  8. return index % (num * 2) <num;
  9. }
  10. });
  11. })(jQuery);

以上代码告诉jQuery:group是一个有效的字符串,可以放在一个冒号的后面构成选择符表达式。而在遇到这个选择符表达式的时候,应该调用给定的函数,用以决定相应的元素是否应该包含在结果集中。

这个被求值的函数一共接收了4个参数。

  • element:当前考虑的DOM元素。这个参数对于大多数选择符都是必须的,但我们这个选择符则不需要。

  • index:DOM元素在结果集中的索引。

  • matches:数组,包含用于解析这个选择符的正则表达式的解析结果。一般来说,matches[3]是这个数组中唯一有用的值;假设有一个选择符的形式为:group(b),则matches[3]中包含的值就是b,也就是括号中的文本。

  • set:匹配到当前元素的整个DOM元素集合。这个参数很少用。

伪类选择符需要使用包含在这4个参数中的信息,决定当前元素是否应该包含在结果集中。在我们这个例子中,只需要indexmatches这两个参数。

定义了:group选择符之后,就有了交替元素分组的更灵活的方式。比如,代码清单9-5中的选择符表达式与.filter()函数可以组合为一个选择符表达式:$('#news tr:group(2)')。而对于代码清单9-7的例子,也可以让表格中的每一部分行为保持一致,只要在调用.filter()函数的时候传入:group()表达式即可。甚至,只要向其圆括号中传入不同的数值,就可以改变每一组的行数,如代码清单9-9所示。

代码清单9-9

  1. $(document).ready(function() {
  2. function stripe() {
  3. $('#news').find('tr.alt').removeClass('alt');
  4. $('#news tbody').each(function() {
  5. $(this).children(':visible').has('td')
  6. .filter(':group(3)').addClass('alt');
  7. });
  8. }
  9. stripe();
  10. });

这样,就可以得到如图9-6所示的三行一组的条纹效果。

9.2 定制与优化选择符 - 图1

图 9-6

9.2.2 选择符的性能问题

在规划任何Web项目的时候,都需要考虑项目周期、维护代码的难易程度和效率,以及用户使用网站过程中的性能等问题。通常,前两个问题比第三个问题更重要。特别是对客户端脚本编程来说,开发人员经常落入“过早优化”和“微观优化”的陷阱之中。无数个小时的时间投入进去,换来的往往只有JavaScript代码执行过程中毫秒级别的提升,这种提升也很难被用户的眼睛觉察到。

 开发人员中有一条经验法则,那就是人的时间总比机器的时间更值钱——除非应用程序确实明显反应迟钝。

即使是真的存在性能问题,在jQuery代码中查出瓶颈所在也是非常困难的。正如本章开头所提醒的,有些选择符相对会更快一些。因此,把某些选择符替换成遍历方法可以节省查找页面元素的时间。换句话说,选择符及遍历的性能问题经常是解决用户感觉网页反应迟钝的一个突破口。

 针对选择符和遍历速度所作的任何决定,都有可能伴随着更新更快的浏览器发布,或者jQuery新版本加入巧妙的速度优化而变得毫无价值。为了真正提升性能,最好反复思考自己假定的条件,然后在使用jsPerf(http://jsperf.com/)等工具实际测量之后,再动手编写优化代码。

了解了这些常识,接下来我们就介绍两个简单的指导方针。

  • Sizzle的选择符实现

本章一开始跟大家提到过,在把一个选择符表达式传递给$()函数时,jQuery的Sizzle引擎会解析这个表达式,并确定如何收集该表达式所表示的元素。在最本质的层次上,Sizzle会应用浏览器支持的最高效的原生DOM方法取得nodeList。这个节点列表是一个包含DOM元素的类似数组的对象,jQuery最终会将这个对象转换成真正的数组,并将其添加到jQuery对象中。下面就是jQuery内部使用的几个DOM方法,同时给出了支持它们的浏览器及版本。

方  法**选择目标**支持的浏览器.getElementById()取得ID与给定的字符串匹配的一个元素全部.getElementsByTagName()取得标签名与给定的字符串匹配的所有元素全部.getElementsByClassName()取得某个类名与给定的字符串匹配的所有元素IE 9+、Firefox 3+、Safari 4+、Chrome 4+和Opera 10+.querySelectorAll()取得与给定的选择符表达式匹配的所有元素IE 8+、Firefox 3.5+、Safari 3+、Chrome 4+和Opera 10+

在这些方法都不能处理某个选择符表达式的情况下,Sizzle会退而求其次地循环遍历已经收集到的所有元素,并根据这个表达式来测试每一个元素。具体来说,假如没有现成的DOM方法可以拿来处理这个选择符表达式,Sizzle就会使用document.getElementsByTagName('*')来取得文档中的全部元素,然后再遍历并测试每个元素。

与使用任何一个原生DOM方法相比,这种遍历和测试每个元素的方法十分影响性能。好就好在,所有现代浏览器的最新版本都开始原生支持.querySelectorAll()方法了,此时Sizzle就会在其他(也许更快的)原生方法不可用的情况下使用这个方法。但也有一个例外:如果选择符表达式中包含自定义的jQuery选择符(例如:eq():odd:even),而这些选择符并没有对应的CSS版本,那Sizzle也别无选择,只能循环加测试了。

  • 测试选择符的速度

为了让大家对使用.querySelectorAll()方法和使用循环加测试的方法的性能差异有个直观的了解,我们假设想要找到一个文档中的所有<input type="text">元素。为此,可以使用两种选择符表达式,一种是CSS属性选择符$('input[type="text"]'),另一种jQuery自定义选择符$('input:text')。为测试选择符中我们关注的部分,我们要去掉其中的input,只比较$('[type="text"]')$(':text')的速度。使用这两种表达式在JavaScript基准测试网站http://jsperf.com/中进行测试,结果相当有戏剧性。

在jsPerf测试中,每个测试用例都会被反复循环,从而得到指定的时间内能够完成多少次循环。因此,结果数字越大,说明速度越快。用来测试的现代浏览器(Chrome 26、Firefox 20和 Safari 6)由于支持选择符可以利用的.querySelectorAll()方法,平均速度比使用jQuery自定义选择符快得多,如图9-7所示。

9.2 定制与优化选择符 - 图2

图 9-7

然而,在不支持.querySelectorAll()的浏览器(如IE7)中,这两个选择符的速度几乎相同。因为这时候两个选择符都会强迫jQuery遍历页面中的每一个元素,并逐个进行测试,如图9-8。

9.2 定制与优化选择符 - 图3

图 9-8

图9-9展示的是测试$('input:eq(1)')$('input').eq(1)这两个选择符的结果,可以看到支持原生方法和不支持原生方法的浏览器性能差异更大。

9.2 定制与优化选择符 - 图4

图 9-9

虽然浏览器每秒的运算次数不一样,但把自定义的:eq()选择符挪到外面来变成.eq()方法,速度提升也是相当明显的。对于这个例子来说,使用简单的input标签名作为$()函数的参数,可以让查找的速度提高很多。而.eq()方法也只不过是调用一个数组处理函数,从jQuery集合中取得第二个元素罢了。

告诉大家一条通用的经验法则:要尽可能使用CSS规范中规定的选择符,除非没有可使用jQuery的自定义选择符。同样,在修改选择符之前,也要记住只在确实有必要提升性能的情况下再去提升。至于测量修改选择符之后的性能提升了多少,可以使用类似http://jsperf.com/所提供的基准测试工具。