您的位置:首页 > Web前端 > JQuery

关于jquery对象的remove参数中出现伪位置类选择器,出现非预期结果的研究

2011-08-21 15:55 337 查看
记得前几天有人在论坛发帖问了一个关于jquery删除节点的问题原帖是这样的(原帖的地址是:原帖)

<ul>

<li>1</li>

<lititle="a">2</li>

<li>3</li>

<li>4</li>

</ul>

$("ulli:eq(1)").remove();//删除了第2个,正常

$("ulli").remove("ulli:eq(1)");//结果只剩下第4个li为毛啊这步

$("ulli").remove("ulli[title='a']");//删除了第2个,正常

这是为何?

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

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

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

jQuery.each({

//keepDataisforinternaluseonly--donotdocument

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其实说白了就是一个函数函数原型为

varSizzle=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中寻找最左边的字符。挺抽象的举个例子:

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

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

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

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

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

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

遍历第四个元素时调用filter(“ulli: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){

varp_result=newArray();

varseeds=makeArray(seed);

vari;

for(i=0;i<seeds.length;i++)

{

varj;

for(j=0;j<set.length;j++){

if(seeds[i]==set[j])

p_result.push(seeds[i]);

}

}

set=p_result;

}



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

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

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

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

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

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

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

综上所诉:

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

总之,不要在remove方法的选择符参数中含有伪位置选择符,它不是想看起来那么工作的!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐