jQuery选择器探究:Sizzle构造函数

来源:互联网 发布:mac mini macbook air 编辑:程序博客网 时间:2024/05/16 11:49
(jquery1.6.1)什么情况下jQuery会调用传统的通用的Sizzle逻辑呢?前几篇分析过,ID选择器会在jQuery构造函数中直接被处理,TAG选择器和CLASS选择器会在新式浏览器中(支持并实现document.querySelectorAll强大功能)直接被处理,甚至只要遵循CSS规范的选择器都会被新式浏览器直接处理,那么剩下的场景将交给传统Sizzle引擎处理了:不支持document.querySelectorAll强大功能的浏览器下的定位查找、jQuery专有的选择器语法等。不得不说,在浏览器实现落后于ES规范的较早的年代才是Sizzle引擎真正独领风骚大放异彩的时代,尽管英雄迟暮,但是其仍然有独到之处,还是非常值得研究的(比如用java语言按照类似的设计实现一套通用的数据ETL方案),接下来的系列将深入探究Sizzle机制和实现。

一 Sizzle构造函数签名

Sizzle构造函数签名如下:“var Sizzle = function( selector, context, results, seed ) {...}”,前三个参数都是必须的,分别描述了"以什么选择规则""在哪个对象中"执行选择后"存入哪个结果对象中",至于第四个参数seed比较特殊,正常情况下不传递这个参数即可,某些特殊场景下用以替换context对象,即在指定的seed对象中执行选择,这个参数后续讨论。
直接分析Sizzle函数被调用的逻辑--位于5137行的代码,这里是给jQuery原型对象扩展find函数,详细定义如下:
find: function( selector ) {var self = this,i, l;if ( typeof selector !== "string" ) {return jQuery( selector ).filter(function() {for ( i = 0, l = self.length; i < l; i++ ) {if ( jQuery.contains( self[ i ], this ) ) {return true;}}});}var ret = this.pushStack( "", "find", selector ),length, n, r;for ( i = 0, l = this.length; i < l; i++ ) {length = ret.length;jQuery.find( selector, this[i], ret );if ( i > 0 ) {// Make sure that the results are uniquefor ( n = length; n < ret.length; n++ ) {for ( r = 0; r < length; r++ ) {if ( ret[r] === ret[n] ) {ret.splice(n--, 1);break;}}}}}return ret;},

可以看到,调用jQuery.find方法也即调用Sizzle函数(参见5109行:'jQuery.find = Sizzle;',并且排除重载的Sizzle逻辑之后)。
一般我们敲入$("xxx")后,jQuery会将"xxx"作为选择符传递给Sizzle的第一个参数,这个参数将在Sizzle函数内部被详细分析。
第三个参数传入的对象是'var ret = this.pushStack( "", "find", selector )',pushStack是jQuery原型中的一个通用方法,这样调用'( "", "find", selector )'简单来说就是生成一个空的jQuery对象,"选择"被执行后的结果集将被inject到这个空的jQuery对象中。
第二个参数context由jQuery传入已被选中的原生对象,换一种说法是,我们执行的任何选择逻辑必然有一个上下文环境,即"从哪个对象中选择",这也是第二个参数名为context的原因,最顶层的上下文环境是document,当然也可以是从之前已被选中的结果集中继续定位查找新的结果集。我们看到jQuery.find(也即Sizzle函数)是在循环中被调用的,每次循环传入this[i],this即jQuery对象,i是其索引属性(参照对jQuery对象的分析篇),也就是从jQuery已选中结果集中轮询原生dom对象并定位查找新的结果集,并且每次对一个元素执行一次查找后都会对ret对象执行去重操作。

二 Sizzle对选择器的处理

分析Sizzle函数的第一步(忽略对参数的判断):
var m, set, checkSet, extra, ret, cur, pop, i,prune = true,contextXML = Sizzle.isXML( context ),parts = [],soFar = selector;// Reset the position of the chunker regexp (start from head)do {chunker.exec( "" );m = chunker.exec( soFar );if ( m ) {soFar = m[3];parts.push( m[1] );if ( m[2] ) {extra = m[3];break;}}} while ( m );

这段代码的逻辑比较清晰:使用正则表达式chunker分析选择符selector,根据selector的特征,如果不是组合选择器(即选择器中有逗号','识别符号),则将selector分割成一个个独立的基本选择器并依次存入parts数组中(其顺序在后续逻辑中非常重要);如果在对选择器执行切割时发现逗号(即发现组合选择器),则将其后组合的选择器存入extra变量中并执行Sizzle递归调用。
这里有一个比较丑陋的调用'chunker.exec( "" );',每次执行剩余字符串正则匹配前需要重置chunker正则对象,因为chunker有g标识,这实际上是一种非常不优雅的且降低效率的方式,一种更简单快速的方式是直接调用'chunker.lastIndex = 0;'

这里的重点是chunker正则表达式,前文已经单独分析过,这里重点分析,这个正则表达式对象(连同Expr.match对象的各种正则表达式对象)是理解Sizzle核心模块道路上的第一道难关。
((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)

拆分开来,其实就三个捕获分组,第二个分组作用是识别是否为组合选择器,捕获特殊符合逗号以及前后空格:'(\s*,\s*)?'这个分组是用问号限定修饰。
第三个分组用星号限定修饰,捕获内容是除第一分组外的任意字符串,甚至包括\r和\n,因此重点是第一个分组。
第一个分组分两种情况,要么是以下选择符之一(占一个独立字符位置):[>+~],这三个字符都将会被Sizzle特殊对待,因为他们是css层次选择器的特殊语法,'>'表示父子选择,'+'表示兄弟选择,'~'表示同辈选择(还有一种层次选择器是祖孙选择器,其特殊字符就是一个空格,这里没有单独罗列空格字符([ >+~])是因为空格本身比较特殊,再加上Sizzle的特定实现逻辑支持祖孙选择器)。要么是以下四种选择器之一:'\((?:\([^()]+\)|[^()]+)+\)','\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]','\\.','[^ >+~,(\[\\]+',第一种选择器是用圆括号括起来的语法,这种选择器一般用于位置过滤选择器或者子元素过滤选择器,其实可以更简洁一些:'\((?:\(?[^()]+\)?)+\)';第二种选择器是用方括号括起来的语法,这种选择器一般用于属性过滤选择器,也分为三种情况,拆分后其实并不那么复杂,这里不展开了;第三种选择器就更简单了,仅仅是一个反斜杠(已被转义)配一个通配符,这种选择器场景比较少见,究竟有什么卵用目前还不得而知;第四种选择器更通用,除了前面三种外都可以被它捕获,比如常见的标签选择器,jQuery专有的选择器等等,这种选择器的定义是用'排除某些字符'的方式来定义的,这里有几点需要特别注意,一是它没有排除冒号,这就说明以冒号开始的过滤选择器将被它捕获,它也没有排除#符号和.符号,所以ID选择器和CLASS选择器也可以被它捕获,另一个点是一个大坑--它排除掉空格了!实际执行中发现,非组合复杂选择器将以空格为界限被切割,但是chunker正则表达式中又没有明显的找到空格标识符,其实这里是有的,只是没有显式定义,而是直接写入一个空格符号,在[^ >+~,(\[\\]+中取反符号^后的第一个位置,这种做法很让人费解,毕竟\s是可以匹配所有中英文甚至半角全角空格符的,虽然直接空格符也都匹配,但是\s毕竟更易阅读理解,所以第四种选择器稍微调整一个字符更友好:'[^\s>+~,(\[\\]+'。需要特别注意的是,这四种选择器并不是互斥的,因为包裹它们的外围非捕获分组是贪婪模式,这意味着类似:input:not(:gt(3))的选择器将被一次性匹配,而不是被切割成更细的多个子选择器。

顺便说一句,对于chunker正则对象的最直观理解就是用各种不同的选择符去测试它。

经过chunker对selector的处理后,我们得到一个关于拆分后的基本选择器的有序的"栈",说它是有序队列也可以,因为它实际上是由一个数组parts实现的。Sizzle将会区别对待parts这个数据结构,某些选择器场景下将它视作栈,对其元素执行"先进后出、后进先出"的FILO策略;某些选择器场景下将它视作队列,对其元素执行"先进先出、后进后出"的FIFO策略。这个执行策略甚至可以说是Sizzle引擎得以实现的关键(在落后的浏览器中实现先进复杂的选择),鉴于Sizzle的核心逻辑就在"Sizzle的查找和过滤",且场景较多、涉及到复杂的递归调用,接下来将按照场景拆分成多篇文章深入探究。
0 0
原创粉丝点击