一、前言

Sizzle原来是jQuery里面的选择器引擎,后来逐渐独立出来,成为一个独立的模块,可以自由地引入到其他类库中。我曾经将其作为YUI3里面的一个module,用起来畅通无阻,没有任何障碍。Sizzle发展到现在,以jQuery1.8为分水岭,大体上可以分为两个阶段,后面的版本中引入了编译函数的概念,Sizzle的源码变得更加难读、不再兼容低版本浏览器,而且看起来更加零散。本次阅读的是Sizzle第一个阶段的最终版本jQuery1.7,从中收获颇多,一方面是框架设计的思路,另外一方面是编程的技巧。

二、jQuery constructor

Sizzle来源于jQuery,并且jQuery是一个基于DOM操作的类库,那么在研究Sizzle之前很有必要看看jQuery的整体结构:

  1. (function(window, undefined) {
  2. var jQuery = function (selector, context) {
  3. return new jQuery.fn.init(selector, context, rootjQuery);
  4. }
  5. jQuery.fn = jQuery.prototype = {...}
  6. jQuery.fn.init.prototype = jQuery.fn;
  7. // utilities method
  8. // Deferred
  9. // support
  10. // Data cache
  11. // queue
  12. // Attribute
  13. // Event
  14. // Sizzle about 2k lines
  15. // DOM
  16. // css operation
  17. // Ajax
  18. // animation
  19. // position account
  20. window.jQuery = window.$ = jQuery;
  21. })

jQuery具有很强的工程性,一个接口可以处理多种输入,是jQuery容易上手的主要原因,相应的,这样一个功能庞大的API内部实现也是相当复杂。要想弄清楚jQuery与Sizzle之间的关系,首先就必须从jQuery的构造函数入手。经过整理,理清楚了构造函数的处理逻辑,在下面的表中,jQuery的构造函数要处理6大类情况,但是只有在处理选择器表达式(selector expression)时才会调用Sizzle选择器引擎。

三、Sizzle的设计思路

对于一个复杂的选择器表达式(下文的讨论前提是浏览器不支持querySelectorAll) ,如何对其进行处理?

3.1 分割解析

对于复杂的选择器表达式,原生的API无法直接对其进行解析,但是却可以对其中的某些单元进行操作,那么很自然就可以采取先局部后整体的策略:把复杂的选择器表达式拆分成一个个块表达式和块间关系。在下图中可以看到,1、选择器表达式是依据块间关系进行分割拆分的;2、块表达式里面有很多伪类表达式,这是Sizzle的一大亮点,而且还可以对伪类进行自定义,表现出很强的工程性;3、拆分后的块表达式有可能是简单选择器、属性选择器、伪类表达式的组合,例如div.a、.a[name = "beijing"]。

3.2  块表达式查找

表达式拆分成一个个块表达式后,接下来的工作就是求出结果集合了。在3.1中已经声明过,此时的块表达式也可能是复杂的选择器表达式,那该怎么处理组合的块表达式呢?

a. 依据API的性能查找:对于程序开发人员而言,代码的效率是一个永恒的主题,那此时查询的依据自然要依赖于选择的性能。在DOM的API中,ID > Class > Name> Tag。

b. 块内过滤:上述步骤中只是依据块表达式的一部分进行了查询,显然得到的集合范围过大,有些不符合条件,那么接下来就需要对上述得到的元素集合进行块内过滤。

总结:此环节包括两个环节,查找+[过滤]。对于简单的块表达式,显然是不需要过滤的。

3.3  块间关系处理

经过块内查找, 得到了一个基本的元素集合,那如何处理块间关系呢?通过观察可以发现,对一个复杂的选择器表达式存在两种顺序:

  • 从左到右:对得到的集合,进行内部逐个遍历,得到新的元素集合,只要还有剩余的代码块,就需要不断地重复查找、过滤的操作。总结下就是:多次查找、过滤。
  • 从右到左:对得到的元素集合,肯定包括了最终的元素,而且还有多余的、不符合条件的元素,那么接下来的工作就是不断过滤,把不符合条件的元素剔除掉。

对于“相邻的兄弟关系(+)”、“之后的兄弟关系(~)”,哪种方式都无所谓了,效率没什么区别。但是对于“父子关系”、“祖先后代关系”就不一样了,此时Sizzle选择的是以从右到左为主,下面从两个维度进行解释:

a、设计思路

  • 左到右:不断查询,不断缩小上下文,不断地得到新的元素集合
  • 右到左:一次查询,多次过滤,第一查找得到的元素集合不断缩小,知道得到最终的集合

b、DOM树

  • 左到右:从DOM的上层往底层进行的,需要不断遍历子元素或后代元素,而一个元素节点的子元素或后代元素的个数是未知的或数量较多的
  • 右到左:从DOM的底层往上层进行的,需要不断遍历父元素或祖先元素,而一个元素的父元素或者祖先元素的数量是固定的或者有限的

但是从右到左是违背我们的习惯的,这样做到底会不会出现问题呢?答案是会出现错误,请看下面的一个简单DOM树:

  1. <div>
  2. <p>aa</p>
  3. </div>
  4. <div class=“content”>
  5. <p>bb</p>
  6. <p>cc</p>
  7. </div>
  8. 求$(‘.content > p:first’)的元素集合?
  9.  
  10. 首先进行分割: ‘.content > p:first’---> ['.content', '>', 'p:first']
  11.  
  12. 右-->左
  13. 查找: A = $('p:first') = ['<p>aa</p>']
  14. 过滤: A.parent().isContainClass('content')---> null

在上面的例子中,我们看到当选择器表达式中存在位置伪类的时候,就会出现错误,这种情况下没有办法,准确是第一位,只能选择从左到右。

结论: 从性能触发,采取从右到左; 为了准确性,对位置伪类,只能采取从左到右。

四、Sizzle具体实现

4.1 Sizzle整体结构

  1. if(document.querySelectorAll) {
  2. sizzle = function(query, context) {
  3. return makeArray(context.querySelectorAll(query));
  4. }
  5. } else {
  6. sizzle 引擎实现,主要模拟querySelectorAll
  7. }

通过上述代码可以看到,Sizzle选择器引擎的主要工作就是向上兼容querySelectorAll这个API,假如所有浏览器都支持该API,那Sizzle就没有存在的必要性了。

关键函数介绍:

  • Sizzle = function(selector, context, result, seed) : Sizzle引擎的入口函数

  • Sizzle.find: 主查找函数

  • Sizzle.filter: 主过滤函数

  • Sizzle.selectors.relative: 块间关系处理函数集   {“+”: function() {}, “ ”:function() {}, “>”: function() {}, “~”: function() {}}

4.2 分割解析

  1. chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g

就是靠这样一个正则表达式,就可以把复杂多样的选择器表达式分割成若干个块表达式和块间关系,是不是觉得块表达式是一项神奇的技术,可以把复杂问题抽象化。正则的缺点是不利于阅读和维护,下图对其进行图形分析:

再来看看是如何具体实现的呢:

  1. do {
  2. chunker.exec( "" ); // chunker.lastIndex = 0
  3. m = chunker.exec( soFar );
  4. if ( m ) {
  5. soFar = m[3];
  6. parts.push( m[1] );
  7. if ( m[2] ) {
  8. extra = m[3];
  9. break;
  10. }
  11. }
  12. } while ( m );
  13.  
  14. for example
  15. $(‘#J-con ul>li:gt(2)’) 解析后的结果为:
  16. parts = ["#J-con", "ul", ">", li:gt(2)"]
  17. extra = undefined
  18.  
  19. $(‘#J-con ul>li:gt(2), div.menu’) 解析后的结果为:
  20. parts = ["#J-con", "ul", ">", “li:gt(2)"]
  21. extra = div.menu

4.3 块表达式处理

4.3.1 块内查找

在查找环节,通过Sizzle.find来实现,主要逻辑如下:

  • 依据DOM API性能决定查找依据: ID > Class> Name> Tag, 其中要考虑浏览器是否支持getElementsByClassName
  • Expr.leftMatch:确定块表达式类型
  • Expr.find:具体的查找实现
  • 结果: {set: 结果集合, expr: 块表达式剩余的部分,用于下一步的块内过滤}
  1. // Expr.order = [“ID”, [ “CLASS”], “NAME”, “TAG ]
  2. for ( i = 0, len = Expr.order.length; i < len; i++ ) {
  3. ……
  4. if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
  5. set = Expr.find[ type ]( match, context);
  6. expr = expr.replace( Expr.match[ type ], "" );
  7. }
  8. }

4.3.2块内过滤

该过程通过Sizzle.filter来进行,该API不仅可以进行块内过滤,还可以进行块间过滤,通过inplace参数来确定。主要逻辑如下:

  • Expr.filter: {PSEUDO, CHILD, ID, TAG, CLASS, ATTR, POS} , 选则器表达式的类型
  • Expr.preFilter: 过滤前预处理,保证格式的规范化
  • Expr.filter: 过滤的具体实现对象
  • 内过滤、块间从左到后: inplace=false,返回新对象;块间从右到左: inplace=true, 原来的元素集合上过滤
  1. Sizzle.filter = function( expr, set, inplace, not ) {
  2. for ( type in Expr.filter ) { //filter: {PSEUDO, CHILD, ID, TAG, CLASS, ATTR, POS}
  3. // Expr.leftMatch:确定selector的类型
  4. if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
  5. // 过滤前预处理,保证格式的规范化
  6. match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
  7. // 进行过滤操作
  8. found = Expr.filter[ type ]( item, match, i, curLoop );
  9. // if inplace== true,得到新数组对象;
  10. if ( inplace && found != null ) {
  11. if ( pass ) { anyFound = true; } else { curLoop[i] = false; }
  12. } else if ( pass ) {
  13. result.push( item );
  14. }
  15. }
  16. }
  17. }

4.4 块间关系处理

4.4.1 判断处理顺序

满足下面的正则,说明存在位置伪类,为了保证计算的准确定,必须采取从左到后的处理顺序,否则可以为了效率尽情使用从右到左。

  1. origPOS = /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/

4.4.2 左到右处理

首先依据parts的第一个元素进行查询,然后对得到的元素集合进行遍历,利用位置伪类处理函数posProcess进行伪类处理,直到数组parts为空。

  1. // parts是selector expression分割后的数组
  2. set = Expr.relative[ parts[0] ] ? [ context ] : Sizzle( parts.shift(), context );
  3. // 对元素集合多次遍历,不断查找
  4. while(parts.length) {
  5. selector = parts.shift();
  6. ……
  7. set = posProcess(selector, set, seed);
  8. }

接下来在看下posProcess的内部逻辑:如果表达式内部存在位置伪类(例如p:first),在DOM的API中不存在可以处理伪类(:first)的API,这种情况下就先把伪类剔除掉,依照剩余的部分进行查询(p),这样得到一个没有伪类的元素集合,最后在以上述中的伪类为条件,对得到的元素集合进行过滤。

  1. // 从左到后时,位置伪类处理方法
  2. var posProcess = function( selector, context, seed ) {
  3. var match,
  4. tmpSet = [],
  5. later = "",
  6. root = context.nodeType ? [context] : context;
  7. // 先剔除位置伪类,保存在later里面
  8. while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
  9. later += match[0];
  10. selector = selector.replace( Expr.match.PSEUDO, "" );
  11. }
  12.  
  13. selector = Expr.relative[selector] ? selector + "*" : selector;
  14. // 在不存在位置伪类的情况下,进行查找
  15. for ( var i = 0, l = root.length; i < l; i++ ) {
  16. Sizzle( selector, root[i], tmpSet, seed );
  17. }
  18. // 以位置伪类为条件,对结果集合进行过滤
  19. return Sizzle.filter( later, tmpSet );
  20. };

4.4.3 右到左的处理顺序

其实Sizzle不完全是采用从右到左,如果选择器表达式的最左边存在#id选择器,就会首先对最左边进行查询,并将其作为下一步的执行上下文,最终达到缩小上下文的目的,考虑的相当全面。

  1. // 如果selector expression 最左边是#ID,则计算出#ID选择器,缩小执行上下文
  2. if(parts[0] is #id) {
  3. context = Sizzle.find(parts.shift(), context)[0];
  4. }
  5. if (context) {
  6. // 得到最后边块表达式的元素集合
  7. ret = Sizzle.find(parts.pop(), context);
  8. // 对于刚刚得到的元素集合,进行块内元素过滤
  9. set = Sizzle.filter(ret.expr, ret.set) ;
  10. // 不断过滤
  11. while(parts.length) {
  12. pop = parts.pop();
  13. ……
  14. Expr.relative[ cur ]( checkSet, pop );
  15. }
  16. }

对于块间关系的过滤,主要依据Expr.relative来完成的。其处理逻辑关系是:判断此时的选择器表达式是否为tag,如果是则直接比较nodeName,效率大增,否则只能调用Sizzle.filter。下面以相邻的兄弟关系为例进行说明:

  1. "+": function(checkSet, part){
  2. var isPartStr = typeof part === "string",
  3. isTag = isPartStr && !rNonWord.test( part ), //判断过滤selector是否为标签选择器
  4. isPartStrNotTag = isPartStr && !isTag;
  5. if ( isTag ) {
  6. part = part.toLowerCase();
  7. }
  8. for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
  9. if ( (elem = checkSet[i]) ) {
  10. while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
  11. checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
  12. elem || false :
  13. elem === part;
  14. }
  15. }
  16. if ( isPartStrNotTag ) {
  17. Sizzle.filter( part, checkSet, true );
  18. }
  19. }

4.5 拓展性

Sizzle的另外一大特性就是可以自定义选择器,当然仅限于伪类,这是Sizzle工程型很强的另外一种表现:

  1. $.extend($.selectors.filters, {
  2. hasLi: function( elem ) {
  3. return $(elem).find('li').size() > 0;
  4. }
  5. });
  6. var e = $('#J-con :hasLi');
  7. console.log(e.size()); // 1

上述代码中的$.extend相当远YUI3中的augment、extend、mix的合体,功能相当强大,只需要对$.selectors.filters(即Sizzle.selectors.filters)对象

进行拓展就可以,里面每个属性的返回值为Boolean类型,用于判断伪类的类型。 
 

Sizzle选择器引擎介绍的更多相关文章

  1. [转]JQuery - Sizzle选择器引擎原理分析

    原文: https://segmentfault.com/a/1190000003933990 ---------------------------------------------------- ...

  2. jQuery源码分析系列(三)Sizzle选择器引擎-下

    选择函数:select() 看到select()函数,if(match.length === 1){}存在的意义是尽量简化执行步骤,避免compile()函数的调用. 简化操作同样根据tokenize ...

  3. jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——编译原理

    这一节要分析的东东比较复杂,篇幅会比较大,也不知道我描述后能不能让人看明白.这部分的源码我第一次看的时候也比较吃力,现在重头看一遍,再分析一遍,看能否查缺补漏. 看这一部分的源码需要有一个完整的概念后 ...

  4. jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——词法解析

    jQuery源码9600多行,而Sizzle引擎就独占近2000行,占了1/5.Sizzle引擎.jQuery事件机制.ajax是整个jQuery的核心,也是jQuery技术精华的体现.里面的有些策略 ...

  5. jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——总结与性能分析

    Sizzle引擎的主体部分已经分析完毕了,今天为这部分划一个句号. a. Sizzle解析流程总结 是时候该做一个总结了.Sizzle解析的流程已经一目了然了. 1.选择器进入Sizzle( sele ...

  6. jQuery源码分析系列(二)Sizzle选择器引擎-上

    前言 我们继续从init()方法中的find()方法往下看, jQuery.find = Sizzle; ... find: function (selector) { /** ... */ ret ...

  7. jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——一些有用的Sizzle API

    说一下Sizzle中零碎的API.这些API有的被jQuery接管,直接使用jQuery.xxx就可以使用,有的没有被接管,如果要在jQuery中使用,使用方法是jQuery.find.xxx. 具体 ...

  8. jQuery选择器引擎和Sizzle介绍

    一.前言 Sizzle原来是jQuery里面的选择器引擎,后来逐渐独立出来,成为一个独立的模块,可以自由地引入到其他类库中.我曾经将其作为YUI3里面的一个module,用起来畅通无阻,没有任何障碍. ...

  9. sizzle选择器的使用

    <!doctype html> <html> <head> <meta charset="utf-8"> <title> ...

随机推荐

  1. android第一行代码-5.监听器的两种用法和context

    监听器的两种用法 1.匿名函数设置监听器 public class MainActivity extends Activity { private Button button; @Override p ...

  2. iPhone添加邮箱

    阿里云邮箱设置 手机自带的电子邮件客户端该如何添加阿里云邮账号呢?这里以iPhone4s和安卓系统为例,分别进行添加阿里云邮箱帐号的添加.   官网是这么介绍的:   一.如下以iPhone4s为例, ...

  3. IE9下css hack写法

    ie9一出css hack也该更新,以前一直没关注,今天在内部参考群mxclion分享了IE9的css hack,拿出来也分享一下: select { background-color:red\0; ...

  4. 协同js库,代码编辑器

    一些协同的js库 Collabedit, Online Code Editor http://collabedit.com/ Stypi, a realtime editor https://www. ...

  5. [转] form.getForm().submit的用法及Ext.Ajax.request的小小区别

    原文地址:http://blog.csdn.net/hongleidy5000/article/details/7329325 if (!formDetail.getForm().isValid()) ...

  6. Java部署_IntelliJ创建一个可运行的jar包(实践)

    一.本文目的:使用Intellij Idea 13生成一个简单可执行的jar,用于快速在linux验证某个功能 二.项目源码 1.结构图  2.StaticC1.java 1 2 3 4 5 6 7 ...

  7. JS中的进制转换以及作用

    js的进制转换, 分为2进制,8进制,10进制,16进制之间的相互转换, 我们直接利用 对象.toString()即可实现: //10进制转为16进制 ().toString() // =>&q ...

  8. Android基础总结(二)

    常见布局 相对布局 RelativeLayout 组件默认左对齐.顶部对齐 设置组件在指定组件的右边 android:layout_toRightOf="@id/tv1" 设置在指 ...

  9. 解决:ERROR: Cannot launch Jack server

    问题重现: Install: /home/dinphy/sm/out/target/product/ido/system/lib/libdl.so java -Xmx3500m -jar /home/ ...

  10. console.log((function f(n){return ((n > 1) ? n * f(n-1) : n)})(5))调用解析

    console.log((function f(n){) ? n * f(n-) : n)})()); 5被传入到函数,函数内部三元计算,5 > 1成立,运算结果是5*f(4),二次运算,5*4 ...