上一篇讲了jQuery对象,这次要讲的是jQuery最复杂的部分--selector,凡是用过jQuery的想必印象最深刻的还是它的selector,借用CSS的语法,它可以很方便地选择到需要的元素。jQuery支持全部的CSS3的selector,现在主流的浏览器支持才是CSS2,这里不会讲怎么使用selector,而是分析它的源代码,看它是怎么实现的。
在上一篇的末尾我提到,$("selector", context)实际调用的是$(context).find("selector"),那我们现在就来看find方法。如果你搜索源代码,你会发现两处find函数的声明,这里是在jQuery对象上调用的,所以应该是jQuery.fn中的那个find方法,即第288到299行之间的代码:
从上面的分析,我们可以得到jQuery对象的find方法最终调方法用了jQuery.find函数,这里要区分一下jQuery对象的方法和jQuery的函数,如果你有面向对象经验(如Java),那么你可以简单认为前者是实例方法,后者是类方法,就实现上来说,前者的方法定义在jQuery.fn对象中,后者定义在jQuery对象中。jQuery实现中还有很多这样的将jQuery对象的方法委托给jQuery中的方法的情况。jQuery.find又是Sizzle的别名(第2364行),jQuery的selector实现采用的是Sizzle框架,所以当我们说jQuery的selector其实就是在说Sizzle。让我们看Sizzle函数的源代码:
Sizzle返回满足selector的所有元素,它接受4个参数,第一个是css selector,是一个字符串;第二个是context,它是单个元素(DomElement或者DomDocument),css selector就是基于它的,即Sizzle方法返回的元素都是它的child元素(没有seed参数时),context可选,默认为document;第三个参数为results,如果它不为空,满足selector的元素会添加到results中,Sizzle方法返回results引用;最后一个参数是seed,它是一个元素数组,这时css selector是基于seed中每个元素,这个参数仅在内部使用,它不是Sizzle公开API的一部分,当这个参数有值时通常context为空,且selector不含位置filter,即:nth/:eq, :first/:last, :even/:odd, :lt/:gt,注意:nth-child,first-child, last-child等不是位置filter。
接着看Sizzle的实现,它的前9行代码(1426-1434)没什么实质内容,前两行设置默认值,接下来判断context只能为Element或Document,然后确保selector为字符串。1439-1448行主要是将selector用chunk正则表达式切分成多个part,到底是如何切分的呢?简单地说,它会在空格、逗号、关系操作符(>, +, ~)处分开。例如“div:first>p"会切分成["div>first", ">", "p"],"div p:last"会切分成["div", "p:last"],大家可以用正则表达式测试工具来试验chunk。如果selector在逗号处分开(1444行),则会递归调用Sizzle函数,例如Sizzle("div, p")会先调用Sizzle("div"),然后调用Sizzle("p"),最后将两者的结果合并起来,并去掉重复的元素(1525-1538行)。在1444行判断m[2]不为空(必定为逗号),则跳出循环,即暂时只处理逗号前面的一部分selector,剩下的赋给extra。
接下来的处理根据selector是否含有包含多个part且有位置filter分为两种情况,如下图代码所示:
第1450行进行的就是这样的判断,为什么需要这样的判断呢?举个例子,有如下的HTML:
$("div p:first")只会返回["#p1"],:first是位置filter,它首先得到$("div p")然后取第一个元素,而$("div p:first-child")则返回["#p1", "#p3"],它返回div元素下所有是第一个孩子的p元素,两者的区别在于位置filter的结果依赖于它前面的selector解析的结果,而其它filter(如属性filter,伪filter),只依赖于当前元素本身,即根据当前元素本身就可以判断它是否满足filter。比如对于伪filter,:first-child,为了判断某个元素是不是第一个孩子元素,只需要取得它的父元素的第一个孩子元素,看它们两者是否相同就可以了。对于关系操作符,又有些不同,例如selector,"div > p",对于某个元素p,除了元素本身之外,它还需要知道关系的另一端,即“div",才能判断这个元素是否满足关系。这样,也就是说对于非位置filter或关系,我们只需要知道少量信息(最多两个)就可以判断某个元素是否满足filter或关系。这样,如果selector的所有part都不含位置filter,我们可以从后往前解析,例如$("div p:first-child"),我们可以首先取得所有为第一个孩子的p元素,然后再看它的父元素是否为div元素。而对于$("div p:first")则必须从前往后解析,即先得到所有的div元素,然后对每个div元素得到其下所有的p元素,将这些p元素合并,然后再取第一个p元素。jQuery正是这样处理的。
我们先来看selector不含位置filter的情况,即1468-1493之间的代码。第1468行到1470行,从parts中移除末尾的part进行处理,如果没有seed,先调用Sizzle.find对末尾的part进行预处理,即尽可能先缩小要处理的元素范围,它返回剩下的还没处理的expr和待过滤的元素。如果有seed,则不用预处理了,待过滤的元素就是seed。Sizzle.find的源代码如下:
它接受三个参数,第一个是expr,即selector,但只能包含一个part(还记得chunk正则表达式吗?),第二个是context,是一个DomElement,第三个是bool类型的参数isXml,表示处理的是Xml还是HTML,两者之间的主要区别在于HTML的tag不区分大小写。Sizzle.find主要流程就是按照Expr.order规定的表达式类型顺序去处理(for循环),当发现第一个匹配的表达式类型时(1558行),就用相应的在Expr.find定义的处理器去处理(1563行),并从expr去掉已经处理的部分(1565行),然后取出循环(1566行)。1561行处理对特殊字符的转义,例如"\#abc"(这个字符串用JavaScript来表达应该写成"\\#abc",以下不另说明),尽管它匹配Expr.match.ID,但由于#有个转义字符"\",因此它并不是一个ID,类似的"\.abc"也不是一个CLASS。1562行也是处理转义的问题,”#abc\.def",它匹配ID,但它的ID是"abc.def“,即要去掉转义字符"\"。最后,如果不能进行任何处理(1572行),则待过滤元素为context下的所有元素(1573行),即最大的可能元素集合。
和Sizzle.find函数相关的Expr.order, Expr.match, Expr.find的代码如下:
从上面可以看出,Sizzle.find会按ID, NAME, TAG的顺序来处理,这是有理由的,因为根据ID来查找效率最高,其次是根据NAME,然后是根据TAG,对于某些浏览器还会根据CLASS来查找,如果它支持getElementsByClassName方法,这部分的处理是在2231-2252行的代码完成,这里就不列出了。
回到Sizzle函数的1470行,那里的三元条件判断表达式是什么意思呢?考虑这样一种调用,Sizzle("~ p", aDiv),它是要找到所有aDiv后面的兄弟p元素,如果将Sizzle.find函数的context参数设为aDiv,待过滤的p元素集合将是aDiv.getElementsByTag("p"),而实际上应该是aDiv.parentNode.getElementsByTag("p"),这就是1470行的意义所在。第1471行对待过滤的元素集合用剩下的表达式进行过滤,得到的满足selector最后一个part的所有元素集合。Sizzle.filter做的事情很复杂,它的源代码为:
Sizzle.filter接受四个参数,其中后两个参数可选。如果没有后两个参数(或者它们都为false),它对set中的元素进行过滤,返回所有满足expr的元素。如果inplace为true,则直接修改set,如果某个位置的元素不满足expr,则将该位置的值设成为false。如果not为true,相当于反转结果,返回所有不满足expr的元素。Sizzle.filter实现的思想是每次处理一部分expr(最外层while循环,1639-1648行保证每次循环必须处理一部分expr),如何处理呢?它会遍历Expr.filter定义的filter(1584行的for循环),用第一个能够处理的filter处理,处理完成之后便结束当前循环(1623-1635行),每次迭代结果保存在curLoop中。对每个filter可能有个相应的preFilter(1593行),它进行部分预处理,preFilter和filter的主要区别在于preFilter对整个curLoop进行处理,filter对curLoop中的单个元素进行处理。preFilter可以改变传给filter的match参数的值(1594行),如果preFilter返回值为false,表示preFilter就可以搞定了,用不着filter了(1596-1597行)。如果preFilter严格返回true,表示该filter不能处理,要给下一个filter去处理(1598-1599行),这种发生在PSEUDO的preFilter中,因为POS也匹配PSEUDO的正则表达式,当发生这种情况时PSEUDO应该放弃处理。preFilter返回其他值时(1603行),对curLoop中的每一个元素遍历(1606行),并对每个元素调用filter函数(1606),根据其返回结果及not参数决定该元素是否满足expr(1607行),然后根据inplace参数决定是直接更改curLoop还是将元素添加到results中(1609-1618行)。当处理完expr后,即expr为空字符串,返回最后一次的迭代结果curLoop。
接下来让我们来分析几个preFilter和filter,先看preFilter:
我们看到ID的preFilter很简单,只是处理了一下转义符。PSUDEO的preFilter则只处理了not伪filter,处理完后返回false,即不再需要filter的处理。注意由于POS和CHILD也可匹配PSUDEO的match正则表达式,因此这里要忽略掉它们,所以要返回true(1842-1844行),对于其它的情况,不改变match直接返回。我感觉jQuery中的preFilter和filter的职责分得并不是很清楚,一般来说preFilter处理整个curLoop,当然它也可以对curLoop的每个元素进行遍历,这样就相当于完成了filter的功能,例如CLASS的preFilter就是这样处理的(限于空间,这里没有列出来),它对curLoop遍历之后,最后返回false,意味不用调用每个元素的filter了。按照现在的设计,我觉得针对单个元素的操作还是应该放在filter中做,当然也有可能是理解不周,作者可能还有其它的考虑。对于其它的preFilter,虽然有些实现还蛮有趣(例如CHILD),但也不再介绍,只要有了概念,应该都是容易看懂的。
对于filter,只看PSEUDO,因为它是jQuery选择器的主要扩展点。
我们可以看到它会根据伪filter名字到Expr.filters中去找相应的伪filter处理器,如果找到则调用它(1940-1944行),这意味着我们很容易添加自定义的伪filter处理器,大家可以参考Expr.filters中伪filter处理器,这里也不再列出。然后再处理contains,not伪处理器,我不理解为什么这些不都在Expr.filters中处理,not在这里处理还有道理可言,因为PSUDEO的preFilter中对not伪Filter改变了match,在这里处理可以表明它们之间的强关联,而contains在这里处理就完全没有道理了,还是我理解得不够透彻?!
好了,我已经扯得太远了,让我们继续回到Sizzle方法中来。当调用完Sizzle.filter后(1471行) ,我们已经处理完selector最后一个part了,返回的元素集合为set,记住,我们是从后往前处理的,因此我们才完成了第一步。如果还有part没有处理完(1473行),拷贝一份set给checkSet,如果已经处理完成(1475行),则设置prune为false,这只是一种优化手段,字面意思是,这就是最后结果了,不用再剪枝啦!现在我们处理完单个part了,但我们还要处理part之间的关系。checkSet到底代表什么呢?我们还是来举个例子吧,让我们来分析Sizzle("div > p input")的执行过程,我们知道首先要将"div > p input"分解成多个part,即["div", ">", "p", "input"],根据1468-1477行的执行过程,我们知道set为document中的所有input元素集合,即document.getElementsByTag("input"),checkSet为set的一个浅拷贝。接下来我们取出下一个part,如果下一个part是关系,我们还要取出一个part,这正是1480-1486行的目的。对于我们的例子,只取出一个part,即"p",关系为“”,即ancestor-descendant关系,接下来是第1492行的处理,它根据关系类型调用Expr.relative中相应的处理器,对于我们的例子,它的作用就是遍历所有的checkSet(包含所有的input元素),找到最近的一个祖先p元素,如果找到则将相应位置的input元素替换为该p元素,如果找不到则替换为false。这样处理之后,checkSet要么为p元素,要么为false。接着处理剩下的part,这次发现取出的是一个关系操作符>,所以还要取出一个part,即"div",Expr.relative[">"]的处理是遍历checkSet中的每个元素,如果它不为false,是它的直接父元素是否为一个div元素,如果是则将checkSet相应位置的p元素替换为div元素,否则替换为false。这样处理之后,checkSet要么为div元素,要么为false。现在已经处理完所有part了。剩下的事情就是遍历checkSet,看它哪些元素不为false(div元素),则将set中对应位置的元素(p元素)添加到results中,这正是1504-1522行的作用。
当然,我举的例子只是一种极简单的情况,考虑第1488-1490行的代码,pop是可能为空的,这时pop不是一个字符串的selector,而是一个元素,例如,Sizzle("> p", aDiv),这时pop就是aDiv。即使pop是个selector,它也可能不只是一个简单的tag selector,它可能包含其它复杂的selector,例如Sizzle("div.green > p"),这时就不能只判断p元素的父结点是个div了,而要先找到所有$("div.green")的元素,然后再看p元素的父结点是否是其中的一个。Expr.relative中定义的处理器得考虑这几种情况。只看">"关系操作符的处理器,这是最简单的一个处理器,但弄清楚了这个,其它的也并不难理解。
1700行判断part是不是一个字符串(即一个选择器),1702行进一步判断它是不是只是一个tag,如果是的话,只需要对checkSet每个不为false的元素,判断它的nodeName是不是等于part,不等于则将checkSet对应位置的元素设置为false(1703-1709行)。1713到1724处理另外两种情况,如果part为元素,只需判断checkSet的元素是否和part元素引用相等(1718行),如果part为复杂选择器,则将checkSet的每个不为false的元素用它的父结点替换(1717行),并在1723行调用Sizzle.filter来对checkSet用part选择器进行过滤,其中inplace参数设置为true。
到现在为止,我已经详细说明了Sizzle方法中当selector仅包含一个part或者包含多个part但不包含位置filter时的执行过程。现在来看第二种情况,即selector含多个part且包含位置参数的情形,即1450-1467行的代码:
我前面已经说明了当selector包含位置filter时,其处理是从前往后处理的,并且像不包含位置filter的情形一样,也是一对关系一对关系来处理的。第1451判断仅有两个part,且第1个part为关系操作符,当Sizzle("> p:first", aDiv)会发生这种情形。其它情况,1454-1456行建立了一个初始集合,当第一个part为关系操作符,它就是仅包含context一个元素的数组,否则就是这个满足这个part选择器的所有元素集合,即Sizzle(part, context)的返回结果。接下来对剩下的part,每次取一个关系操作符(如果有的话)和下一个part(1459-1462行)。posProcess到底做什么呢?简单地说,它就是对set中每个元素用作context来对selector进行选择,并将所有得到的元素集合合并。我们来看看它的源代码:
2349-2352行正如注释中所说,首先要去掉selector其中的位置filter(也可能包含在not伪filter中),其实现是将所匹配PSEUDO正则表达式的filter(包括位置filter和伪filter)给从selector中去掉了,这不影响结果,被去掉的伪filter置于later中。为什么需要这样做呢?考虑这样一种情况,posProcess("p:first", [div1, div2]),如果我们不先去掉其中的:first位置filter,我们就会先取出div1下的第一个p元素,然后取出div2下的第一个p元素,然后再将两个合并,会得到两个p元素(前提是两个div下都有一个p元素),这显然不是我们想要的结果。我们希望的结果是先忽略其中的:first,把div1和div2下的所有p合并,然后取出对它们用:first进行过滤,取出第一个p元素。明白这,剩下的代码就不难理解了。第2356-2368行对所有context中元素遍历,查看其下满足selector的所有元素。第2360行对这些元素使用第一步去掉的伪filter进行过滤,并返回过滤后的结果。
总算结束啦!我已经讲完了jQuery选择器主要实现,剩下的就只是一些细枝末节了,稍加用心应该就能看明白。最后总结一下,其实现是将selector分成多个part,然后根据selector是否至少包含两个part并且有位置filter来分成两种情况,对于是的情况,对分解后的多个part从前往后处理,对于否的情况,对分解后的多个part从后往前处理。
0 评论:
发表评论