3.4 Sizzle( selector, context, results, seed )
函数Sizzle( selector, context, results, seed )用于查找与选择器表达式selector匹配的元素集合。该函数是选择器引擎的入口。
函数Sizzle( selector, context, results, seed )执行的6个关键步骤如下:
1)解析块表达式和块间关系符。
2)如果存在位置伪类,则从左向右查找:
a.?查找第一个块表达式匹配的元素集合,得到第一个上下文元素集合。
b.?遍历剩余的块表达式和块间关系符,不断缩小上下文元素集合。
3)否则从右向左查找:
a.?查找最后一个块表达式匹配的元素集合,得到候选集、映射集。
b.?遍历剩余的块表达式和块间关系符,对映射集执行块间关系过滤。
4)根据映射集筛选候选集,将最终匹配的元素放入结果集。
5)如果存在并列选择器表达式,则递归调用Sizzle( selector, context, results, seed )查找匹配的元素集合,并合并、排序、去重。
6)最后返回结果集。
下面来看看该函数的源码实现。
1.?定义Sizzle( selector, context, results, seed )
相关代码如下所示:
3879 var Sizzle = function( selector, context, results, seed ) {
第3879行:定义函数Sizzle( selector, context, results, seed ),接受4个参数:
参数selector:CSS选择器表达式。
参数context:DOM元素或文档对象,作为查找元素的上下文,用于限定查找范围。默认值是当前文档对象。
参数results:可选的数组或类数组,函数Sizzle( selector, context, results, seed )将把查找到的元素添加到其中。
参数seed:可选的元素集合,函数Sizzle( selector, context, results, seed )将从该元素集合中过滤出匹配选择器表达式的元素集合。
2.?修正参数results、context
相关代码如下所示:
3880 results = results || [];
3881 context = context || document;
3882
3883 var origContext = context;
3884
3885 if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
3886 return [];
3887 }
3888
3889 if ( !selector || typeof selector !== "string" ) {
3890 return results;
3891 }
3892
第3880行:如果未传入参数results,则默认为空数组[]。方法.find( selector )调用Sizzle ( selector, context, results, seed )时会传入一个jQuery对象,匹配元素将会被添加到传入的jQuery对象中。
第3881行:如果未传入参数context,则默认为当前document对象。
第3883行:备份上下文context。因为如果参数selector是以#id开头的,可能会把上下文修正为#id所匹配的元素。这里备份的origContext用于存在并列选择器表达式的情况。
第3885~3887行:如果参数context不是元素,也不是document对象,则忽略本次查询,直接返回空数组[]。
第3889~3891行:如果参数selector是空字符串,或者不是字符串,则忽略本次查询,直接返回传入的参数results。
3.?定义局部变量
相关代码如下所示:
3893 var m, set, checkSet, extra, ret, cur, pop, i,
3894 prune = true,
3895 contextXML = Sizzle.isXML( context ),
3896 parts = [],
3897 soFar = selector;
3898
第3893~3897行:定义一组局部变量,它们的含义和用途如下:
变量m:用于存放正则chunker每次匹配选择器表达式selector的结果。
变量set:在从右向左的查找方式中,变量set称为“候选集”,是最后一个块表达式匹配的元素集合,其他块表达式和块间关系符则会对候选集set进行过滤;对于从左向右的查找方式,变量set是当前块表达式匹配的元素集合,也是下一个块表达式的上下文。
变量checkSet:对于从右向左的查找方式,变量checkSet称为“映射集”,其初始值是候选集set的副本,其他块表达式和块间关系符则会对映射集checkSet进行过滤,过滤时先根据块间关系符将其中的元素替换为父元素、祖先元素或兄弟元素,然后把与块表达式不匹配的元素替换为false,最后根据映射集checkSet筛选候选集set;对于从右向左的查找方式,事实上在查找过程中并不涉及变量checkSet,只是在函数Sizzle()的最后为了统一筛选和合并匹配元素的代码,将变量checkSet与变量set指向了同一个数组。
变量extra:用于存储选择器表达式中第一个逗号之后的其他并列选择器表达式。如果存在并列选择器表达式,则会递归调用函数Sizzle( selector, context, results, seed )查找匹配元素集合,并执行合并、排序和去重操作。
变量ret:只在从右向左执行方式中用到,用于存放查找器Sizzle.find( expr, context, isXML )对最后一个块表达式的查找结果,格式为{ expr:“...”, set: array }。
变量pop:只在从右向左的查找方式中用到,表示单个块表达式。
变量prune:只在从右向左的查找方式中用到,表示候选集set是否需要筛选,默认为true,表示需要筛选,如果选择器表达式中只有一个块表达式,则变量prune为false。
变量contextXML:表示上下文context是否是XML文档。
变量parts:存放了正则chunker从选择器表达式中提取的块表达式和块间关系符。
变量soFar:用于保存正则chunker每次从选择器表达式中提取了块表达式或块间关系符后的剩余部分,初始值为完整的选择器表达式。
4.?解析块表达式和块间关系符
相关代码如下所示:
3860 var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
3899 // Reset the position of the chunker regexp (start from head)
3900 do {
3901 chunker.exec( "" );
3902 m = chunker.exec( soFar );
3903
3904 if ( m ) {
3905 soFar = m[3];
3906
3907 parts.push( m[1] );
3908
3909 if ( m[2] ) {
3910 extra = m[3];
3911 break;
3912 }
3913 }
3914 } while ( m );
3915
第3900~3914行:用正则chunker从选择器表达式中提取块表达式和块间关系符,直到全部提取完毕或遇到下一个并列选择器表达式为止。正则chunker称为“分割器”,含有3个分组:块表达式或块间关系符、逗号、选择器表达式剩余部分,这也是Sizzle中最长、最复杂、最关键的正则,具体将在3.5节单独介绍和分析。
第3901~3902行:正则chunker每次匹配选择器表达式的剩余部分之前,先通过匹配一个空字符来重置正则chunker的开始匹配位置,从而使得每次匹配时都会从头开始匹配。直接设置“chunker.lastIndex = 0;”也能达到同样的效果。
第3904~3913行:如果正则chunker可以匹配选择器表达式的剩余部分,则将第三个分组(即经过当前匹配后的剩余部分)赋予变量soFar,下次do-while循环时继续匹配;通过这种方式也可过滤掉一些垃圾字符(如空格);同时,将第一个分组中的块表达式或块间关系符插入数组parts中;此外,如果第二个分组不是空字符串,即遇到了逗号,表示接下来是一个并列选择器表达式,则将第三个分组保存在变量extra,然后结束循环。
5.?如果存在位置伪类,则从左向右查找
相关代码如下所示:
3916 if ( parts.length > 1 && origPOS.exec( selector ) ) {
3917
3918 if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
3919 set = posProcess( parts[0] + parts[1], context, seed );
3920
3921 } else {
3922 set = Expr.relative[ parts[0] ] ?
3923 [ context ] :
3924 Sizzle( parts.shift(), context );
3925
3926 while ( parts.length ) {
3927 selector = parts.shift();
3928
3929 if ( Expr.relative[ selector ] ) {
3930 selector += parts.shift();
3931 }
3932
3933 set = posProcess( selector, set, seed );
3934 }
3935 }
3936
4221 var Expr = Sizzle.selectors = {
4224 match: {
4231 POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,
4233 },
4749 };
4751 var origPOS = Expr.match.POS,
第3916~3935行:如果存在块间关系符(即相邻的块表达式之间有依赖关系)和位置伪类,例如,$('div button:first'),则从左向右查找。正则origPOS中定义了所支持的位置伪类,见第4231行。
为什么遇到位置伪类需要从左向右查找呢?以$( "div button:first" )为例,在查找所有div元素下的所有button元素中的第一个时,位置伪类":first"过滤的是"div button"匹配的元素集合,因此,必须从左向右查找,并且需要先从选择器表达式中删除位置伪类,然后执行查找,最后才用位置伪类过滤查找结果。这个过程由函数posProcess( selector, context, seed )实现。
第3918~3919行:如果数组parts中只有两个元素,并且第一个是块间关系符,则可以直接调用函数posProcess( selector, context, seed )查找匹配的元素集合。
第3921~3935行:否则,从左向右对数组parts中的其他块表达式逐个进行查找,每次查找时指定前一个块表达式匹配的元素集合作为上下文,即不断缩小查找范围。
第3922~3924行:首先,从数组parts头部弹出第一个块表达式,递归调用函数Sizzle( selector, context, results, seed )查找匹配元素集合,得到第一个上下文元素集合;如果数组parts的第一个元素是块间关系符,则直接把参数context作为第一个上下文元素集合。
第3926~3934行:从左向右遍历数组parts中的其他块表达式和块间关系符,调用函数posProcess( selector, context, seed )查找匹配元素集合,调用时传入的参数selector含有一个块间关系符和一个块表达式,并且指定上下文为前一个块表达式匹配的元素集合set,调用后再将返回值赋值给变量set,作为下一个块表达式的上下文。
posProcess( selector, context, seed )
函数posProcess( selector, context, seed )在指定的上下文数组context下,查找与选择器表达式selector匹配的元素集合,并且支持位置伪类。选择器表达式selector由一个块间关系符和一个块表达式组成。
函数posProcess( selector, context, seed )执行的3个关键步骤如下:
1)删除选择器表达式中的所有伪类。
2)调用Sizzle( selector, context, results, seed )查找删除伪类后的选择器表达式所匹配的元素集合。
3)调用 Sizzle.filter( expr, set, inplace, not )用伪类过滤查找结果。
下面来看看该函数的源码实现。相关代码如下所示:
5266 var posProcess = function( selector, context, seed ) {
5267 var match,
5268 tmpSet = [],
5269 later = "",
5270 root = context.nodeType ? [context] : context;
5271
5272 // Position selectors must be done after the filter
5273 // And so must :not(positional) so we move all PSEUDOs to the end
5274 while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
5275 later += match[0];
5276 selector = selector.replace( Expr.match.PSEUDO, "" );
5277 }
5278
5279 selector = Expr.relative[selector] ? selector + "*" : selector;
5280
5281 for ( var i = 0, l = root.length; i < l; i++ ) {
5282 Sizzle( selector, root[i], tmpSet, seed );
5283 }
5284
5285 return Sizzle.filter( later, tmpSet );
5286 };
第5274~5277行:删除选择器表达式中的所有伪类,并累计在变量 later 中。
第5279行:如果删除伪类后的选择器表达式只剩一个块间关系符,则追加一个通配符"*"。
第5281~5283行:遍历上下文数组,调用函数Sizzle( selector, context, results, seed )查找删除伪类后的选择器表达式匹配的元素集合,将查找结果合并到数组tmpSet 中。
第5285行:调用方法Sizzle.filter( expr, set, inplace, not )用记录的伪类later 过滤元素集合tmpSet,并返回一个新数组,其中只包含了过滤后的元素。
下面回到对函数Sizzle( selector, context, results, seed )的分析中。
6.?如果不存在位置伪类,则从右向左查找
(1)尝试缩小查找范围
相关代码如下所示:
3937 } else {
3938 // Take a shortcut and set the context if the root selector is an ID
3939 // (but not if it'll be faster if the inner selector is an ID)
3940 if ( !seed && parts.length > 1 &&
context.nodeType === 9 &&
!contextXML &&
3941 Expr.match.ID.test(parts[0]) &&
!Expr.match.ID.test(parts[parts.length - 1]) ) {
3942
3943 ret = Sizzle.find( parts.shift(), context, contextXML );
3944 context = ret.expr ?
3945 Sizzle.filter( ret.expr, ret.set )[0] :
3946 ret.set[0];
3947 }
3948
3949 if ( context ) {
// 省略从右向左查找的代码
3982 } else {
3983 checkSet = parts = [];
3984 }
3985 }
3986
第3940~3947行:如果第一个块选择器是 ID 类型(即格式为#id),并且最后一个块选择器不是ID 类型,则修正上下文 context 为第一个块选择器匹配的元素,以缩小查找范围,提高查找效率。在这个过程中,先调用方法 Sizzle.find( expr, context, isXML ) 对第一个块表达式执行简单的查找,如果还有剩余部分,再调用方法 Sizzle.filter( expr, set, inplace, not ) 对查找结果进行过滤,最后取匹配元素集合的第一个元素作为后续查找的上下文。
第3982~3984行:如果第一个块表达式是ID类型,但是没有找到匹配的元素,则没有继续查找和过滤的必要了,此时直接清空数组parts,并设置映射集为空数组。
(2)查找最后一个块表达式匹配的元素集合,得到候选集set、映射集checkSet
相关代码如下所示:
3949 if ( context ) {
3950 ret = seed ?
3951 { expr: parts.pop(), set: makeArray(seed) } :
3952 Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
3953
3954 set = ret.expr ?
3955 Sizzle.filter( ret.expr, ret.set ) :
3956 ret.set;
3957
3958 if ( parts.length > 0 ) {
3959 checkSet = makeArray( set );
3960
3961 } else {
3962 prune = false;
3963 }
3964
第3950~3956行:查找最后一个块表达式匹配的元素集合,得到候选集 set。先调用方法Sizzle.find( expr, context, isXML )对最后一个块表达式执行简单的查找,如果还有剩余部分,再调用方法Sizzle.filter( expr, set, inplace, not )对查找结果进行过滤。
如果传入了参数seed,则不需要调用Sizzle.find()查找,只调用Sizzle.filter()过滤。
第3958~3963行:如果数组parts中还有其他元素,即还有块表达式或块间关系符,则创建一份候选集set的副本,并赋值给checkSet,作为映射集;如果数组parts为空,则表示选择器表达式中只有一个块表达式,此时设置变量prune为false,表示不需要对候选集set进行筛选。
(3)遍历剩余的块表达式和块间关系符,对映射集checkSet执行块间关系过滤
相关代码如下所示:
3965 while ( parts.length ) {
3966 cur = parts.pop();
3967 pop = cur;
3968
3969 if ( !Expr.relative[ cur ] ) {
3970 cur = "";
3971 } else {
3972 pop = parts.pop();
3973 }
3974
3975 if ( pop == null ) {
3976 pop = context;
3977 }
3978
3979 Expr.relative[ cur ]( checkSet, pop, contextXML );
3980 }
3981
第3965~3980行:从右向左遍历数组 parts 中剩余的块表达式和块间关系符,调用块间关系符在 Sizzle.selectors.relative 中对应的过滤函数,对映射集 checkSet 执行块间关系过滤,直至数组 parts 为空为止。
第3966~3973行:变量 cur 表示块间关系符,变量pop 表示块间关系符左侧的块表达式。每次遍历时,如果弹出的元素不是块间关系符,则默认为后代关系符;如果弹出的元素是块间关系符,则再弹出一个作为块表达式。因为是从右向左查找,所以变量 pop 的作用是作为过滤映射集checkSet 的上下文。
第3975~3977行:如果仍然未找到前一个块表达式 pop,则表示已经到达数组头部,直接将上下文context 作为映射集checkSet 的上下文。
第3979行:块间关系过滤函数的参数格式为:
Sizzle.selectors.relative[ 块间关系符 cur ]( 映射集 checkSet, 左侧块表达式 pop, contextXML );
在块间关系过滤函数中,会先根据块间关系符cur的类型将映射集checkSet的元素替换为父元素、祖先元素或兄弟元素,然后将与左侧块表达式pop不匹配的元素替换为false,具体请参见3.8节。
7.?根据映射集checkSet筛选候选集set,将最终的匹配元素放入结果集results
相关代码如下所示:
3987 if ( !checkSet ) {
3988 checkSet = set;
3989 }
3990
3991 if ( !checkSet ) {
3992 Sizzle.error( cur || selector );
3993 }
3994
3995 if ( toString.call(checkSet) === "[object Array]" ) {
3996 if ( !prune ) {
3997 results.push.apply( results, checkSet );
3998
3999 } else if ( context && context.nodeType === 1 ) {
4000 for ( i = 0; checkSet[i] != null; i++ ) {
4001 if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {
4002 results.push( set[i] );
4003 }
4004 }
4005
4006 } else {
4007 for ( i = 0; checkSet[i] != null; i++ ) {
4008 if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
4009 results.push( set[i] );
4010 }
4011 }
4012 }
4013
4014 } else {
4015 makeArray( checkSet, results );
4016 }
4017
第3987~3989行:在前面查找匹配元素集合的过程中,如果是从左向右查找的,不会涉及映射集 checkSet;如果是从右向左查找的,且只有一个块表达式,也不会对于映射集 checkSet 赋值。在这两种情况下,为了统一筛选和合并匹配元素的代码,在这里要先设置映射集 checkSet 和候选集set 指向同一个数组。
第3995~4012行:如果映射集 checkSet 是数组,则遍历映射集checkSet,检查其中的元素是否满足匹配条件,如果满足,则将候选集set 中对应的元素放入结果集results。
第3996~3997行:如果变量 prune 为false,表示不需要筛选候选集set,则直接将映射集checkSet 插入结果集results 中。注意这里的隐藏逻辑:当选择器表达式中只有一个块表达式时,才会设置变量 prune 为false,此时映射集checkSet 和候选集set 指向同一个数组,见第3958~3963行、第3987~3989行的说明。
第3999~4004行:如果上下文是元素,而不是文档对象,则遍历映射集checkSet,如果其中的元素满足以下条件之一,则将候选集set中对应的元素放入结果集results:
是true。
是元素,并且包含在上下文context中。
第4006~4012行:如果上下文是文档对象,则遍历映射集checkSet,如果其中的元素满足以下全部条件,则将候选集set中对应的元素放入结果集results:
不是null。
是元素。
第4014~4016行:如果候选集checkSet不是数组,则可能是NodeList,这种情况只会在选择器表达式仅仅是简单的标签或类样式(如$( "div" )、$( ".red" ) )时才会出现,此时不需要筛选候选集set,并且映射集checkSet和候选集set会指向同一个数组,可以直接将映射集checkSet插入结果集results中。见第3958~3963行、第3987~3989行的说明。
8.?如果存在并列选择器表达式,则递归调用Sizzle( selector, context, results, seed )查找匹配的元素集合,并合并、排序、去重
相关代码如下所示:
4018 if ( extra ) {
4019 Sizzle( extra, origContext, results, seed );
4020 Sizzle.uniqueSort( results );
4021 }
4022
第4020行:方法Sizzle.uniqueSort( results )负责对元素集合中的元素排序、去重,具体请参见3.10.1节。
9.?最后返回结果集results
相关代码如下所示:
4023 return results;
4024 };
函数Sizzle( selector, context, results, seed )的执行过程可以总结为图3-3。