2.4 解析 jQuery 选择器引擎 Sizzle
jQuery 从 1.3 版本开始,使用了新的选择器引擎 Sizzle(官方网址 http://sizzlejs.com) 。Sizzle 是 jQuery 作者 John Resig 开发的 DOM 选择器引擎 (Dom Selector Engine),速度号称业界第一。而且它有一个重要的特点就是 Sizzle 是完全独立于 jQuery 的,如果用户不想用 jQuery ,还可以只用 Sizzle 。
Sizzle 选择器引擎目前成为 jQuery 框架默认的选择器引擎,相比原来的 jQuery 引擎,速度有很大的提升,如图 2.3 所示的各种选择器执行效率的对比。
2.4.1 回顾CSS的选择器
在解析 jQuery 选择器引擎 Sizzle 之前,我们不妨回顾一下 CSS 的选择器 (CSS selector) 。CSS选择器可以分为三种基本类型:ID选择器 (#id)、Class选择器(.class)和类型(type)选择器(p)。
另外,CSS还支持高级选择器,如属性选择器 (attribute)、伪类或伪对象选择器 (Pseudo Classes) 等。这些都是单一的选择器,可以在应用中把它们组合起来,形成组合选择器,如 div#id,div:last-child。组合型选择器又包括多种关系形式,如包含关系、并列关系、相邻关系和父子关系等。
2.4.2 解析 jQuery 选择器引擎的设计思路
尽管 jQuery 选择器引擎 Sizzle 非常复杂,功能也非常强大,但是它们都是建立在 JavaScript 已定义的方法或属性基础上来实现的。这主要包括元素的 getElementsByTagName() 和 getElementById() 方法,以及元素的 childNodes、firstChild、lastChild、nextSibling、parentNode 和 previousSibling 属性。借助这些方法和属性可以直接或间接地选择相匹配的 DOM 元素。
为了方便讲解,我们结合一个选择器进行说明。选择器代码如下所示。
$("div.red");
这是一个复合选择器,一搬读者可以这样分析:在DOM文档树中找到 class 属性等于 "red" 的 div 元素。即一步到位,直接从DOM文档树中选择所需要的元素。实际上,如果根据选择器引擎的工作方式,可以把这个字符串拆分成两步走。
- 第一步,根据 document.getElementsByTagName() 方法选择文档树中的 div 元素集合。
- 第二步,根据其 class 是否等于 "red" 进行判断,把不等于该值的元素从结果集中去掉。
不仅是这个选择器,对于所有形式的复合选择器,都可以根据这种思路来拆分选择器,然后分别完成每一部分的操作。
不过,对于 jQuery 选择器引擎来说,不同版本在设计思路上也存在一些细微的区别。例如,针对下面的选择器:
$("div p");
早期的 jQuery 选择器是根据下面的步骤进行解析的。
- 第一步,根据 document.getElementsByTagName() 方法选择文档树中的 div 元素集合。
- 第二步,迭代 div 元素集合,在所有的 div 元素中查找每个 div 元素下的 p 元素。
- 第三步,合并结果。
而 Sizzle 选择器适当调整了解析的顺序。
- 第一步,根据 document.getElementsByTagName() 方法选择文档树中的 p 元素集合。
- 第二步,迭代 p 元素集合,在所有的 p 元素中查找每个 p 元素的父级元素。
- 第三步,检测父级元素。如果不是 div 元素,则遍历上一级元素;如果迭代到文档树的顶层,则排除该 p 元素;如果是 div 元素,则保存该 p 元素。
经过比较可以看到,Sizzle 选择器放弃了合并结果操作,直接在遍历过程中过滤元素,所以导致查找速度大幅提升。
初步把握了 jQuery 选择器的基本工作原理,那么我们就可以了解 jQuery 在匹配元素时是如何工作的。例如,对于 $("div[id=value]");选择器,我们知道 JavaScript 先匹配 div 元素,然后再判断 div 元素的 id 属性是否等于 value ,如果不等于就从结果集中删除掉。而对于 $("div#value"); 选择器也是一样,可以看到 div#value 与 div[id=value] 是完全相同的操作。同样, div.class 也可以转换为属性选择符进行操作。
2.4.3 选择器和过滤器
根据上面的分析,我们可以把 CSS选择器拆分为两大组成部分,或者说可以把选择器分为以下两类。
- 第一类,选择器 (selector) 。即根据给定的选择符,从 DOM 文档树找到相关的元素节点,并存储到结果集中。
- 第二类,过滤器 (filter) 。根据表达式的条件,在结果集中过滤元素。
于是,这里就有一个技术难题:哪些选择符适合选择器(selector),哪些选择符适合过滤器(filter) ?
由于可以直接使用 document.getElementsByTagName() 方法进行选择,因此对于类型选择器来说,直接使用选择即可。
类型选择器相互之间还可以组成各种形式的复合选择器。例如:
*
E F
E~F
E+F
E>F
E/F
E
虽然这些特殊形式的类型选择器由多个选择器构成,但是它们都可以从文档中直接选择,故我们可以统一把它们归为选择器类型。而对于 ID 选择器和 Class 选择器来说:
- 如果它们在 selector 字符串的起始位置,那么它们也可以完成选择功能。例如 $("#id");、$(".class");。
- 如果它们不在起始位置,那么就应该作为筛选器实现。例如 $("div#id"); 、$("div.class"); 。
实际上,由于 ID 选择器可以直接调用 JavaScript 的 document.getElementById() 方法进行选择。但是对于 Class 选择器来说,由于它无法直接进行选择,因此,我们可以把ID选择器视为选择器,而把 Class 选择器视为过滤器。
对于 Class 选择器来说,还可以把它转换为 "*.class" 形式。其中的 "*" 表示先选取范围内所有的元素,然后再进行过滤,这样就可以实现统一的编程接口。
对于属性选择器、伪类选择器来说,它们只能够附在其他选择器后面使用,如果单独使用,则可以通过在前面添加 * 标签,以便设计成统一的语法格式,以方便选择器引擎的工作。它们与 Class 选择器的工作方式是相同的,即都视为过滤器。
例如,对于下面这个复杂的选择器来说,包含了类型选择器、Class选择器、ID选择器、属性选择器和伪类选择器。
$("div.red:nth-child(odd)[title=bar]#wrap p");
jQuery 解析的步骤如下。
第一步,选择 DOM 文档树中所有的 p 元素,建立初步结果集。
第二步,在结果集中,选择父级元素为 div 的元素,形成新的结果集。
第三步,在新的结果集中筛选class属性为 red 的元素
第四步,解析伪类选择器 :nth-child(odd),在结果集中筛选元素的子元素为偶数的元素。
第五步,解析属性选择器 [title=bar] ,在结果集中筛选元素的 title 属性为 bar 的元素。
第六步,在结果集中筛选 id 等于 wrap 的元素。
2.4.4 Sizzle 引擎结构
jQuery 的CSS 选择器引擎 Sizzle 共有 1000 多行代码,占据了 jQuery 框架四分之一的份额。
这些代码被直接调用的匿名函数封装在一个独立的空间中,外界是无法访问的。通过下面代码可以把引擎接口传递给 jQuery 空间下的四个公共函数。
jQuery.find = Sizzle;
jQuery.filter = Sizzle.filter;
jQuery.expr = Sizzle.selectors;
jQuery.expr[":"] = jQuery.expr.filters;
Sizzle 引擎在jQuery 框架中的位置犹如咽喉,起到了核心作用,如图 2.4 所示。在下面的 jQuery 选择器逻辑流程图中,
首先,对传入的选择符参数进行过滤,
只有是表达式字符串时,才会进入 jQuery.fn.find() 入口
,然后进入 Sizzle 接口 (jQuery.find()) ,在 Sizzle 构造器中分别调用 Sizzle.find() 和 Sizzle.filter() 函数完成选择和过滤操作。
在选择 (Sizzle.find()) 和过滤 (Sizzle.filter()) 操作过程中,都会经过这样的处理流程:匹配简单的选择器类型 (To match singleselector type) 。
ID 选择器
Name
Class 选择器
类型选择器
........
对于选择器 (Sizzle.find()) 来说,它会调用 Expr.find[type] ,而对于过滤器 (Sizzle.filter()) 来说,它会调用 Expr.filter[type],最后分别返回 Sizzle.find() 和 Sizzle.filter()。
在Sizzle.filter() 过程中,它还需要检测关系选择符是否存在 (To check any relative exisits?) 。如果存在,则调用 Expr.relative[],最后返回结果集。
下面对 Sizzle 引擎的主体结构进行简单的分析。
- <scripttype="text/javascript">
- /*!
- *SizzleCSSSelectorEngine-v0.9.3
- *Copyright2009,TheDojoFoundation
- *ReleasedundertheMIT,BSD,andGPLLicenses.
- *Moreinformation:http://sizzlejs.com/
- */
-
- //把Sizzle引擎封装在一个独立的空间中
- (function(){
- //定义用于块识别器的正则表达式
- varchunker=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^>+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,
- done=0,
- toString=Object.prototype.toString;
- //Sizzle选择器引擎构造器函数
- //参数说明:
- //selector:选择器字符串
- //context:上下文
- //results:结果集
- //seed:种子
- varSizzle=function(selector,context,results,seed){
- //省略的函数体
- };
- //Sizzle匹配函数
- //参数说明:
- //expr:匹配表达式
- //set:条件设置选项
- Sizzle.matches=function(expr,set){
- returnSizzle(expr,null,null,set);
- };
- //Sizzle查询函数
- //参数说明:
- //expr:查询表达式
- //context:上下文
- //isXML:检测函数
- Sizzle.find=function(expr,context,isXML){
- //省略的函数体
- };
- //Sizzle过滤函数
- //参数说明:
- //expr:过滤表达式
- //set:条件设置选项
- //inplace:包含项
- //not:排除项
- Sizzle.filter=function(expr,set,inplace,not){
- //省略的函数体
- };
- //Sizzle表达式对象
- //列举所用的各种匹配表达式
- varExpr=Sizzle.selectors={
- //省略成员属性
- };
- //省略其他辅助性工具函数和逻辑代码暴露的接口
- jQuery.find=Sizzle;
- jQuery.filter=Sizzle.filter;
- jQuery.expr=Sizzle.selectors;
- jQuery.expr[":"]=jQuery.expr.filters;
- //定义的公共函数
- Sizzle.selectors.filters.hidden=function(elem){};
- Sizzle.selectors.filters.visible=function(elem){};
- Sizzle.selectors.filters.animated=function(elem){};
- jQuery.multiFilter=function(expr,elems,not){};
- jQuery.dir=function(elem,dir){};
- jQuery.nth=function(cur,result,dir,elem){};
- jQuery.sibling=function(n,elem){};
- return;
- window.Sizzle=Sizzle;
- })();
- </script>
通过上面的结构分析,可以看到 Sizzle 引擎主要包括一个构造器 Sizzle(),三个核心功能函数 matches()、find() 和 filter() ,以及一个表达式对象 selectors 。下面分别对它们进行讲解。