记得前几天有人在论坛发帖问了一个关于jquery删除节点的问题  原帖是这样的(原帖的地址是:原帖

 

<ul>
<li>1</li>
<li title="a">2</li>
<li>3</li>
<li>4</li>
</ul>
$("ul li:eq(1)").remove( ); // 删除了第2个,正常
$("ul li").remove("ul li:eq(1)"); // 结果只剩下第4个li 为毛啊这步
$("ul li").remove("ul li[title='a']"); // 删除了第2个,正常
这是为何?

 

当时我也回复了,但是当时时间太紧,没有看懂源码,这两天在看jquery的sizzle选择器引擎。然后才想起这个问题可能是jquery的bug,也有可能是出于jquery不鼓励这样的写法。

其实jquery对象调用remove方法的基本流程是这样的:对jquery中的每个dom元素调用remove方法。代码如下

在这个remove方法中重点是对jquery.filter(selector,[this]).这句话是对由当前元素组成的数组由表达式进行过滤,返回数组中都是符合selector的元素。如果过滤后数组依然不为空

jQuery.each({ // keepData is for internal use only--do not document remove: function( selector, keepData ) { if ( !selector || jQuery.filter( selector, [ this ] ).length ) { if ( !keepData && this.nodeType === 1 ) { jQuery.cleanData( this.getElementsByTagName("*") ); jQuery.cleanData( [ this ] ); } if ( this.parentNode ) { this.parentNode.removeChild( this ); } } },

,说明当前元素就是要删除的元素,则 调用this.parentNode.removeChild( this );删除此元素。逻辑上是很简单的。重点就是这个filter是怎么工作的。它的工作方式直接关系到该删除哪个元素。

抛开这些问题先不说,我们使用jquery经常使用$(“xxx”)这样的写法,这个方法就会返回我们想要的包装好地jquery元素,内部是怎么解析地呢?在jQuery1.4后关于这方面的问题已专门由一个开源项目Sizzle,Jquery在遇到上述问题是调用的Sizzle选择器引擎(Sizzle官网:sizzle官网了解更多)。Sizzle其实说白了就是一个函数函数原型为

var Sizzle = function(selector, context, results, seed)

selector就是选择字符串,context是选择执行时的上下文,如果contex为null则默认为document,Result是结果集,如果不为空则将结果附加在其后,seed就是种子集,可以理解为所有的匹配结果都是从seed中取的(即最后得到的结果集肯定是seed的子集)。

Sizzle对有伪位置选择器()和无位置伪选择器是不同的处理顺序,如果selector匹配 /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/这个正则表达式说明选择器字符中若有伪位置选择器,则selector从左至右解析,如果不是则从右往左解析。

如果是从右往左解析,我们很容易想到:在开始解析的时候就会在seed中寻找最左边的字符。挺抽象的举个例子:

“ul li[title=’a’]”为选择字符串,第三个li DOM元素为seed ,从右往左解析就会从seed中过滤不符合条件li[title=’a’]的元素,然后再过滤父元素不是ul的元素。最后得到结果,当然结果大家都知道是为空。

然而如果从左往右解析式,不可能像上面那样做,它只有一步一步从左往右解析,这样得到的最后结果是没有考虑seed的情况还是上面的seed,选择字符串替换为”ul li:eq(1)”,则jquery最后返回的结果就是第二个li元素,完全没有考虑seed给出的范围。

现在我们看上面的那个帖子,remove对每个jquery对象中的每个元素,遍历调用remove方法,如果过滤参数中含有伪位置选择调用符则jQuery.filter(selector,[this]),而此函数此时不会考虑[this]给的范围而直接返回selector选择的结果。

遍历第一个元素时调用filter(“ul li:eq(1)”,[第一个li元素]),此时ul中是有第二个li元素的,故返回innerhtml为2的li DOM元素,此时返回结果集长度不为0,故删除第一个元素。

遍历第二个元素时调用filter(“ul li:eq(1)”,[第一个li元素])(为什么是第一个li元素呢,因为原先第一个li因为符合条件被删了),,此时ul中也是有第二个li元素,故返回innerhtml为3的li DOM元素 ,此时返回结果集长度不为0,故删除‘第一个’元素。

遍历第二个元素时调用filter(“ul li:eq(1)”,[第一个li元素])(原因同上),此时ul中也是有第二个li元素,故返回innerhtml为4的li DOM元素 ,此时返回结果集长度不为0,故删除‘第一个’元素。

遍历第四个元素时调用filter(“ul li:eq(1)”,[第一个li元素])(原因同上),此时ul下只有自己一个li元素故返回的结果集长度为零,不符合条件故不删除。

分析完了,应该明白了为什么会出现上述的奇怪现象。

那怎么让filter按照我们想的那样工作呢。其实就是修改Sizzle的源码。

其实你可能已经猜到,出现上述问题的主要原因是没有使用seed,我们可以在选择字符串从左至右解析完,对解析的结果进行遍历,看是不是seed中的元素如果不是就踢掉,这样就能返回我们以为正确的结果。

 

if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { set = posProcess( parts[0] + parts[1], context ); } else { set = Expr.relative[ parts[0] ] ? [ context ] : Sizzle( parts.shift(), context );   while ( parts.length ) { selector = parts.shift();   if ( Expr.relative[ selector ] ) { selector += parts.shift(); } set = posProcess( selector, set ); } }

这段代码就是用来处理有伪位置选择符的选择字符串的。set就是每步的结果。

我增加的代码加在这之后

修改代码如下   if(parts.length==0&&seed){ var p_result=new Array(); var seeds=makeArray(seed); var i; for(i=0;i&lt;seeds.length;i++) { var j; for(j=0;j&lt;set.length;j++){ if(seeds[i]==set[j]) p_result.push(seeds[i]); } } set=p_result; }  

这样得到的set就是我们想要的结果。

我在运行上面的例子是,结果只剩下1 这个li元素,你可能已经猜到了。分析如下

遍历第一个元素时调用filter(“ul li:eq(1)”,[第一个li元素]),此时虽然有第二个li元素,但是seed并不包含此元素。故只会但会空集合长度为0.不删除。

遍历第二个元素调用filter(“ul li:eq(1)”,[第二个li元素]),此时ul中是有第二个li元素的,并且seed中也包含此元素,故返回的结果集长度为一,符合条件删除此元素。

遍历第三个元素时调用filter(“ul li:eq(1)”,[第二个li元素])(因为原来的第二个元素符合条件被删了),此时ul中是有第二个li元素的,并且seed中也包含此元素,故返回的结果集长度为一,符合条件删除此元素。

遍历第四个元素时调用filter(“ul li:eq(1)”,[第二个li元素])(原因同上),此时ul中是有第二个li元素的,并且seed中也包含此元素,故返回的结果集长度为一,符合条件删除此元素。

故最后只剩下了1这个li元素。

综上所诉:

虽然我们修改了sizzle中的具体实现,但是得到的结果还是不符合我们预期的结果(我们预期的结果是删除第二个元素),所以思考一下这种情况也许不是jQuery的bug,或许是不提倡在remove中使用伪位置选择符。因为你在用的时候没有该清楚的它是怎么运行的。或许你说还要修改remove算法,那就没有必要了。在加了上面的代码后还是没有符合我们的预期,但是我们分析发现,这种处理是完全符合逻辑的。使我们对删除时的选择器写法产生了误解。

总之,不要在remove方法的选择符参数中含有伪位置选择符,它不是想看起来那么工作的!