上一篇
讲了
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从后往前处理。
分享到:
相关推荐
1. **选择器**: 学习如何使用jQuery选择器精准定位HTML元素,如ID选择器、类选择器、属性选择器等,以及如何组合使用它们。 2. **DOM操作**: 包括元素的添加、删除、复制、移动,以及属性和内容的修改。 3. **事件...
这个压缩包文件"jquery源代码 包括示例 包括示例"显然包含了jQuery的核心源代码以及相关的示例,这对于学习和理解jQuery的工作原理及其用法是非常有价值的。 首先,jQuery的核心源代码是JavaScript的一个模块,它...
开发者可以通过阅读这些源代码,深入理解jQuery的工作原理和内部机制。 jQuery-1.4.2.min.js是jQuery库的压缩版,主要目的是为了减小文件大小,加快网页加载速度。这个版本的代码是经过混淆和压缩的,使得代码更...
《jQuery实战 源代码》是一本专注于讲解jQuery库实际应用和源码解析的专业书籍。jQuery作为一款广泛使用的JavaScript库,极大地简化了DOM操作、事件处理、动画制作以及Ajax交互等任务,使得JavaScript编程变得更加...
- 选择器:jQuery的核心功能之一是通过CSS选择器来选取DOM元素,如`$("#id")`、`$(".class")`和`$("tag")`。 - DOM操作:jQuery提供了一套简便的方法来操作DOM,包括添加、删除和修改元素,如`append()`、`remove...
《锋利的jQuery第二版》是一本专注于jQuery技术的书籍,其源代码是学习和深入理解jQuery核心原理的重要资源。jQuery是一个广泛使用的JavaScript库,它极大地简化了网页的DOM操作、事件处理、动画效果和Ajax交互。这...
《jQuery基础教程 (Learning jQuery) 完整源代码》涵盖了jQuery这一强大JavaScript库的基本概念、核心功能以及实际应用。jQuery简化了HTML文档遍历、事件处理、动画制作和Ajax交互等任务,使得JavaScript编程变得...
7. **性能优化**:jQuery源代码展示了如何通过缓存选择器结果、延迟执行、批量操作等手段提升性能。学习这部分可以帮助开发者写出更高效的jQuery代码。 8. **版本迭代**:jQuery的发展历程也是一个不断优化的过程。...
在阅读和学习《锋利的jQuery源代码》时,建议遵循以下步骤: 1. **预习基础**:确保你对JavaScript基础知识和DOM操作有一定了解。 2. **逐行阅读**:从入口函数开始,逐步了解每一部分代码的功能。 3. **理解封装**...
同时,阅读库的文档将有助于理解其具体用法和API,以便更好地定制和控制选择器的行为。 总的来说,基于jQuery的H5移动端选择器是现代Web开发中不可或缺的一部分,它们提高了移动应用的交互性和用户满意度。通过学习...
在阅读jQuery源代码时,我们需要关注其核心功能模块,如选择器(Selectors)、遍历(Traversing)、DOM操作(Manipulation)、事件处理(Events)和动画(Effects)。 1. **选择器(Selectors)**:jQuery的选择器...
9. **源代码分析**:书中附带的源代码提供了丰富的示例,读者可以下载并进行实践,加深对jQuery的理解。 10. **最新版本特性**:由于是第二版,书中会涉及jQuery的最新版本,讲解新功能和改进,确保读者掌握最新的...
《jQuery 1.2.6 源代码分析》 jQuery 是一个广泛应用于网页开发的JavaScript库,以其简洁的API和强大的功能深受开发者喜爱。在深入理解jQuery的工作原理时,分析其源代码是极其有益的。本文将针对jQuery 1.2.6版本...
《精妙绝伦的jQuery 源代码》一书,无疑是深入理解jQuery核心机制的宝贵资源。这本书通过详细的代码示例,帮助读者剖析jQuery库的内部运作,从而提升JavaScript编程能力。在这里,我们将探讨jQuery的核心概念、设计...
这本书的配套源代码提供了丰富的示例和练习,涵盖了jQuery的各个方面,包括选择器、事件处理、DOM操作、动画效果以及AJAX交互等。 在提供的压缩包文件中,我们可以看到以下几个关键文件: 1. **web.config**:这是...
4. **易用性**:通过简单的jQuery代码即可实现日期选择器的添加和初始化,降低开发难度。 5. **事件支持**:提供丰富的事件接口,如日期选择后的回调函数,便于开发者进行进一步的业务逻辑处理。 ### 使用方法 1. ...
例如,如果一个项目只需要用到日期选择器,那么只需要加载日期选择器相关的代码即可。 其次,jQuery UI提供了丰富的API和事件,允许开发者自定义组件的行为。通过绑定特定的事件,如`open`、`close`、`slide`等,...
源代码中,读者会看到各种选择器的用法,如基本选择器(ID、类、标签名)、层次选择器(后代、子元素、相邻兄弟、同级元素)以及属性选择器等。 jQuery的Ajax功能简化了异步数据请求。书中实例可能涵盖使用`.ajax()...
《HeadFirst jQuery源代码》是深入理解jQuery框架的宝贵资源,源自知名教育品牌Head First Labs的官方网站。这个压缩包中的文件名"0636920012740-master-313fd1c3c3975f0d81216eb22d6d6e55a99363e6"可能代表了项目的...
1. **选择器**: jQuery提供了丰富的CSS选择器,如ID选择器(#id),类选择器(.class),元素选择器(element)等,使得选取DOM元素变得非常简单。 2. **链式操作**: jQuery对象支持链式调用,这意味着一个方法的返回结果...