`
marlonyao
  • 浏览: 248939 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

jQuery源代码阅读(二):选择器

阅读更多

上一篇 讲了 jQuery的构造噐,但忽略了jQuery是如何根据css selector来构造jQuery对象的,这正是这篇文章的内容。凡是用过jQuery的想必印象最深刻的还是它灵活的selector,借用CSS的语法,它可以很方便地选择到需要的元素。jQuery支持全部的CSS3的selector,这里不会讲怎么使用selector,而是分析它的源代码,看它是怎么实现的。

在上一篇的末尾我提到,$("selector ", context )实际调用的是$(context ).find("selector "), 那我们现在就来看find方法。如果你搜索源代码,你会发现两处find函数的声明,这里是在jQuery对象上调用的,所以应该是jQuery.fn中 的那个find方法,即第288到299行之间的代码:

 

 

当jQuery对象元素数组长度为1时,这是通常的情况,就会直接对这个唯一的元素调用jQuery.find方法(第292行)。当我们调 用$("div > p"),相当于调用$(document).find("div > p"),$(document)是一个jQuery对象,它的元素数组只有一个元素,即document。当jQuery对象元素数组长度大于1时,实际 上就是对其中每一个元素调用jQuery.find方法,将它们返回的元素数组合并(map函数),然后去掉重复的元素(unique函数)。在这两种情况下都会得到一个元素数组,最后pushStack方法创建一个新的jQuery对象,其元素数组就是在上一步返回的元素数组,并将它的 prevObject置为当前jQuery对象,它的selector为当前jQuery对象的selector和方法参数中的selector拼接而成。对于第一种情况,看起来是先pushStack然后再find,但是find更新的是jQuery对象内部元素数组,效果其实是一样的。关于map, unique, pushStack这些函数我打算留到以后再说明,但大家仅凭函数名称也应该大致知道它们的功能了,尤其是有过函数式编程语言(如ruby, python)经验的朋友。让我们举个例子来说明一下,$("div").find("> p"),这个语句会创建3个jQuery对象。首先,$("div")等价于$(document).find("div"),它会创建两个jQuery 对象,一个是$(document),为了方便取名为doc,它的selector为空字符串(""),没有prevObject,元素数组仅包含一个 document,记为{selector="", prevObject=undefined, elems=[document]},对doc调用find("div")会查找文档下所有的div元素,假设有两div元素分别为div1和div2, 它又创建了一个jQuery对象,令为divs,它为{selector="div", prevObject=doc, elems=[div1, div2]}。然后对divs调用find("> p"),会创建第3个jQuery对象ps,它为{selector="div > p", prevObject=divs, elems=[p1,p2...]},这三个jQuery对象的context属性都为document。可以看到,创建的三个jQuery形成了一个 stack,即doc<---divs<---ps,每个jQuery对象能够知道这的上一个jQuery对象,可以由end()方法(内部 访问的是prevObject属性)得到,即ps.end() === divs, divs.end() === doc。

从上面的分析,我们可以得到jQuery对象的find方法最终调方法用了jQuery.find函数,这里要区分一下jQuery对象的方法和 jQuery的函数,如果你有面向对象经验(如Java),那么你可以简单认为前者是实例方法,后者是类方法,就实现上来说,前者定义在 jQuery.fn对象中,后者定义在jQuery对象中。jQuery实现中还有很多这样的将jQuery对象的方法委托给jQuery中的方法的情况。jQuery.find又是Sizzle的别名(第2364行),jQuery的selector实现采用的是Sizzle 框架,让我们接着看Sizzle函数的源代码:

 

 


Sizzle返回满足selector的所有元素,它接受4个参数,第一个是css selector,是一个字符串;第二个是context,它是单个元素(DomElement或者DomDocument),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,第二个是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.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中处理。

 

让我们继续回到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是否有位置filter来分成两种情况,对于有位置filter的情况,对分解后的多个part从前往后处理,对于没有位置filter的情况,对分解后的多个part从后往前处理。

分享到:
评论

相关推荐

    jQuery权威指南-源代码

    其次详细讲解了jQuery的各种选择器、jQuery操作DOM的方法、jQuery中的事件与应用、jQuery中的动画和特效、Ajax在jQuery中的应用,以及各种常用的jQuery插件的使用方法和技巧,所有这些知识点都配有完整的示例(包括...

    JQuery权威指南源代码

    使用jQuery选择器实现隔行变色 JavaScript代码检测页面元素 jQuery代码检测页面元素 使用jQuery基本选择器 使用jQuery层次选择器 使用jQuery基本过滤选择器 使用jQuery内容过滤选择器 使用jQuery可见性过滤...

    【JavaScript源代码】JavaScript与JQuery框架基础入门教程.docx

     目录 一,JS对象二,DOM–1,作用–2,测试三,Jquery–1,概述–2,使用步骤–3,入门案例–4,jQuery的文档就绪四,JQuery的语法–1,选择器–2,常用函数–3,常用事件–4,练习总结 一,JS对象 二,DOM –1,...

    jquery颜色选择器源码

    jquery颜色选择器源码,只提供学习和使用

    jquery日期选择器

    轻便好用的JQuery日期控件-Date Input

    JQuery权威指南 源代码

    前 言 第1章 jquery开发入门/1 1.1 jquery概述/2 1.1.1 认识jquery /2 1.1.2 jquery基本功能/2 1.1.3 搭建jquery开发环境/3 1.1.4 编写第一个简单的jquery应用/3 ...jquery 选择器/12 2.1 jquery选择器概述/13

    jquery语法与选择器介绍 带有案例

    jquery语法,与jquery选择器介绍 带有真实的源代码与案例

    jQuery选择器基础知识

    这是一份比较全面的jQuery选择器基础知识,包括源代码和相应的ppt,供大家分享

    jQuerySourceCode:阅读和分析jQuery源代码,以巩固JS知识并学习一些奇妙的技术-jquery source code

    阅读并分析jQuery源代码,以巩固JS知识并学习一些奇妙的技术。 2017.7.5 jQuery源代码(一) 顺一遍jQuery源码的大体逻辑: 一些变量和函数jQuery继承即扩展方法jQuery.extend方法,即jQuery的静态方法,也可以...

    锋利的jQuery书中源代码

    找了很久终于搞到了锋利的jQuery书中源代码,真不错,希望与各位分享:-) 本书内容的编排 jQuery介绍--&gt;选择器--&gt;DOM操作--&gt;事件和动画--&gt;Ajax应用--&gt;插件--&gt;完整DEMO 本书循序渐进的对jQuery的各种方法和使用...

    WEB05-jQuery-源代码.rar

    使用JQuery完成页面定时弹出广告(DOM转换和选择器) 使用JQuery完成表格的隔行换色 使用JQuery完成复选框的全选效果(jQuery的属性操作) 使用JQuery完成省市联动效果(jQuery的遍历和DOM操作) 使用JQuery完成下列列表...

    jQuery权威指南-配套源代码

    其次详细讲解了 jQuery的各种选择器、jQuery操作DOM的方法、jQuery中的事件与应用、jQuery中的动画和特效、Ajax在 jQuery中的应用,以及各种常用的jQuery插件的使用方法和技巧,所有这些知识点都配有完整的示例...

    jQuery:jQuery学习路径源代码-jquery source code

    jQuery学习路径源代码 jQuery是用于dom操纵的客户端库javascript。 该存储库包含文章的源代码,这些文章是Dot Net Tricks jQuery学习路径的一部分。 初学者 中间的 先进的 jQuery Ajax方法 在本文中,我们将了解...

    【JavaScript源代码】js实现类选择器和name属性选择器的示例步骤.docx

     jQuery的出现,大大的提升了我们操作dom的效率,使得我们的开发更上一层楼,如jQuery的选择器就是一个很强大的功能,它包含了类选择器、id选择器、属性选择器、元素选择器、层级选择器、内容筛选选择器等等,很是...

    jQuery技术内幕 深入解析jQuery架构设计与实现原理

    从源代码角度全面而系统地解读了jquery的17个模块的架构设计理念和内部实现原理,旨在帮助读者参透jquery中的实现技巧和技术精髓,同时本书也对广大开发者如何通过阅读源代码来提升编码能力和软件架构能力提供了指导...

    超实用的jQuery代码段

    超实用的jQuery代码段精选近350个jQuery代码段,涵盖页面开发中绝大多数要点、技巧与方法,堪称史上最实用的jQuery代码参考书,可以视为网页设计与网站建设人员的好帮手。《超实用的jQuery代码段》的代码跨平台、跨...

    jquery-table-lottery:九宫格抽奖插件源代码

    自己造的轮子,贴出源代码跟大家交流一下。配置参数: self . def = { html : '' , // 抽奖效果,添加HTML标签 hoverClass : '' , // 抽奖效果,在TD添加样式 startTime : 50 , //起始延迟时间 onlyTime : 2 , ...

    jquery制作的日期选择器

    超漂亮的一个用jquery制作的日期选择器,里面有源代码。

    JavaScript 中使用 jQuery 的货币到 Word 转换器和源代码

    在带有源代码的 JavaScript 中使用 jQuery 的货币到 Word 转换器特征 基本 GUI 该项目包含图形和按钮元素。 基本控制 此项目使用基本控件与应用程序进行交互。 用户友好的界面 这个项目是在一个简单的用户友好的界面...

    JQuery基础案例大全

    JQuery是现在最流行的Ajax框架;本案例是本人亲手总结的教学案例。基本包括的Jquery的各个方面的基础应用。包括:选择器;Dom操作;事件;动画;Ajax操作;是一个非常容易上手的代码案例。送给初学者。--邵老师

Global site tag (gtag.js) - Google Analytics