jQuery技术内幕:深入解析jQuery架构设计与实现原理. 3.4 Sizzle( selector, context, results, seed )

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。

上一篇:Windows Phone 7常用资源大集合


下一篇:【探讨】javascript事件机制底层实现原理