zoukankan      html  css  js  c++  java
  • jQuery 源码分析 7: sizzle

      jQuery使用的是sizzle这个选择器引擎,这个引擎以其高速著称,其实现十分精妙但是也足够复杂,下面现简单分析一下相关的代码。

    在jQuery的部分API接口是直接引用了Sizzle的方法,这些接口如下:

    1 jQuery.find = Sizzle;
    2 jQuery.expr = Sizzle.selectors;
    3 jQuery.expr[":"] = jQuery.expr.pseudos;
    4 jQuery.unique = Sizzle.uniqueSort;
    5 jQuery.text = Sizzle.getText;
    6 jQuery.isXMLDoc = Sizzle.isXML;
    7 jQuery.contains = Sizzle.contains;
      jQuery.find 引用的就是Sizzle,下面看看Sizzle的实现
     
      1  // @param selector 已去掉头尾空白的选择器字符串 
      2  // @param context 执行匹配的最初的上下文(即DOM元素集合)。若context没有赋值,则取document。 
      3  // @param results 已匹配出的部分最终结果。若results没有赋值,则赋予空数组。 
      4  // @param seed 初始集合 
      5 
      6 function Sizzle( selector, context, results, seed ) {
      7      var match, elem, m, nodeType,
      8           // QSA vars
      9           i, groups, old, nid, newContext, newSelector;
     10       if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
     11 
     12           // 根据不同的浏览器环境,设置合适的Expr方法,构造合适的rbuggy测试
     13           setDocument( context );
     14      }
     15       context = context || document;
     16      results = results || [];
     17      nodeType = context.nodeType;
     18       if ( typeof selector !== "string" || !selector ||
     19           nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
     20            return results;
     21      }
     22       if ( !seed && documentIsHTML ) {
     23           // 尽可能快地找到目标节点, 选择器类型是id,标签和类
     24           // rquickExpr = /^(?:#([w-]+)|(w+)|.([w-]+))$/
     25           // 将selector按 #[id] / [tag] / .[class]的顺序捕获到数组中,数组的第一个元素是原始值
     26           // 捕获结果中'#'和'.'会被移除
     27           if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {
     28                // 加速: Sizzle("#ID")
     29                if ( (m = match[1]) ) {
     30                     if ( nodeType === 9 ) {
     31                          elem = context.getElementById( m );
     32                          // 检查Blackberry 4.6返回的已经不在document中的parentNode
     33                          if ( elem && elem.parentNode ) {
     34                               // IE, Opera, Webkit有时候会返回name == m的元素
     35                               if ( elem.id === m ) {
     36                                    results.push( elem );
     37                                    return results;
     38                               }
     39                          } else {
     40                               return results;
     41                          }
     42                     } else {
     43                          // 上下文不是document
     44                          if ( context.ownerDocument && 
     45 
     46                               (elem = context.ownerDocument.getElementById( m )) &&
     47                               contains( context, elem ) && elem.id === m ) {
     48 
     49                               results.push( elem );
     50                               return results;
     51                          }
     52                     }
     53                 // 加速: Sizzle("TAG")
     54                // 由于返回是一个数组,因此需要让这个数组作为参数数组并利用push.apply调用将其拼接到results后面
     55                } else if ( match[2] ) {
     56                     push.apply( results, context.getElementsByTagName( selector ) );
     57                     return results;
     58                // 加速: Sizzle(".CLASS")
     59                // push.apply的使用原因同上
     60                } else if ( (m = match[3]) && support.getElementsByClassName ) {
     61                     push.apply( results, context.getElementsByClassName( m ) );
     62                     return results;
     63                }
     64           }
     65 
     66           // 使用QSA, QSA: querySelectorAll, 原生的QSA运行速度非常快,因此尽可能使用QSA来对CSS选择器进行查询
     67           // querySelectorAll是原生的选择器,但不支持老的浏览器版本, 主要是IE8及以前的浏览器
     68           // rbuggyQSA 保存了用于解决一些浏览器兼容问题的bug修补的正则表达式
     69           // QSA在不同浏览器上运行的效果有差异,表现得非常奇怪,因此对某些selector不能用QSA
     70           // 为了适应不同的浏览器,就需要首先进行浏览器兼容性测试,然后确定测试正则表达式,用rbuggyQSA来确定selector是否能用QSA
     71 
     72           if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
     73                nid = old = expando;
     74                newContext = context;
     75                newSelector = nodeType !== 1 && selector;
     76 
     77                // QSA 在以某个根节点ID为基础的查找中(.rootClass span)表现很奇怪,
     78                // 它会忽略某些selector选项,返回不合适的结果
     79                // 一个比较通常的解决方法是为根节点设置一个额外的id,并以此开始查询
     80                // IE 8 doesn't work on object elements 
     81                if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
     82                     groups = tokenize( selector );                  // 分析选择器的词法并返回一个词法标记数组
     83                     if ( (old = context.getAttribute("id")) ) {     // 保存并设置新id
     84                          nid = old.replace( rescape, "\$&" );
     85                     } else {
     86                          context.setAttribute( "id", nid );
     87                     }
     88                     nid = "[id='" + nid + "'] ";
     89                      i = groups.length;
     90                     while ( i-- ) {
     91                          groups[i] = nid + toSelector( groups[i] );     // 把新的id添加到选择器标记里
     92                     }
     93                     newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context;
     94                     newSelector = groups.join(",");                     // 构造新的选择器
     95                }
     96                 if ( newSelector ) {                                    // 使用新的选择器通过QSA来查询元素
     97                     try {
     98                          push.apply( results,                          // 将查询结果合并到results上
     99                               newContext.querySelectorAll( newSelector )
    100                          );
    101                          return results;
    102                     } catch(qsaError) {
    103                     } finally {
    104                          if ( !old ) {
    105                               context.removeAttribute("id");          // 如果没有旧id,则移除
    106                          }
    107                     }
    108                }
    109           }
    110      }
    111       // 其他selector,这些selector无法直接使用原生的document查询方法
    112      return select( selector.replace( rtrim, "$1" ), context, results, seed );
    113 }
    rbuggy:测试QSA的Bug
     
         使用assert(function(div){})函数进程浏览器bug测试
     1 /**
     2 * Support testing using an element
     3 * @param {Function} fn Passed the created div and expects a boolean result
     4 */
     5 function assert( fn ) {
     6      var div = document.createElement("div");          // 创建测试用节点
     7      try {
     8           return !!fn( div );                          // 转换fn的返回值为boolean值
     9      } catch (e) {
    10           return false;
    11      } finally {
    12           if ( div.parentNode ) {                      // 结束时移除这个节点
    13                div.parentNode.removeChild( div );
    14           }
    15           div = null;                                  // IE浏览器中必须这样,释放内存
    16      }
    17 }
    • assert函数建立一个div节点,将这个div节点传递给回调函数;
    • div节点在assert函数结束时会被删除,此时注意要删除由回调函数创建的子节点,并将div赋值null以让GC回收。
    • 回调函数利用新建的div节点作为根节点,在这个根节点上创建一些测试用的节点进行测试;
    一个bug测试例子:
     1 assert(function( div ) {
     2 
     3      // 创建一些子节点
     4      docElem.appendChild( div ).innerHTML = "<a id='" + expando + "'></a>" +
     5           "<select id='" + expando + "-f]' msallowcapture=''>" +
     6           "<option selected=''></option></select>";
     7      ... // 其他测试
     8      // 测试document.querySelectorAll()的正确性
     9      if ( div.querySelectorAll("[msallowcapture^='']").length ) {
    10           rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|"")" );
    11 
    12           // 确定用于测试selector能否使用QSA的正则表达式
    13 
    14      }
    15      ... // 其他测试 
    16 });
    select方法:
     
         当无法直接使用document的原生选择器时,就会调用Sizzle.select.
         注释里写到"A low-level selection function that works with Sizzle's compiled",这是一个低级选择器,与Sizzle.compiled协作执行.
     
     1 // @param selector 已去掉头尾空白的选择器字符串 
     2 // @param context 执行匹配的最初的上下文(即DOM元素集合)。若context没有赋值,则取document。 
     3 // @param results 已匹配出的部分最终结果。若results没有赋值,则赋予空数组。 
     4 // @param seed 初始集合 
     5 
     6 select = Sizzle.select = function( selector, context, results, seed ) {
     7      var i, tokens, token, type, find,
     8           compiled = typeof selector === "function" && selector,
     9           match = !seed && tokenize( (selector = compiled.selector || selector) );
    10       results = results || [];
    11       // 当没有seed或group时,尽可能地减少操作
    12      if ( match.length === 1 ) {
    13 
    14           // 如果根选择器是id,利用快捷方式并设置context
    15           tokens = match[0] = match[0].slice( 0 );
    16           if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
    17                     support.getById && context.nodeType === 9 && documentIsHTML &&
    18                     Expr.relative[ tokens[1].type ] ) {
    19 
    20              // 使用Expr.find["ID"]查找元素,其中调用了context.getElementById方法
    21              // 为了兼容不同的浏览器,setDocument方法会测试不同的浏览器环境并构造一个使用与当前运行环境的Expr.find["ID"]元素
    22              // 将id选择器的返回结果作为新的上下文
    23 
    24                context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
    25                if ( !context ) {
    26                     return results;     // 如果找不到id根元素直接返回results
    27                 // Precompiled matchers will still verify ancestry, so step up a level
    28                } else if ( compiled ) {
    29                     context = context.parentNode;
    30                }
    31                // 移除第一个id选择器
    32                selector = selector.slice( tokens.shift().value.length );
    33           }
    34           // Fetch a seed set for right-to-left matching
    35           // matchExpr["needsContext"]测试选择器是否含有位置伪类,如:first,:even,或包含"> + ~"等关系
    36 
    37           // 如果包含将i赋值0,否则赋值tokens.length
    38           i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
    39 
    40           // 遍历tokens, 逐个查询
    41           while ( i-- ) {
    42                token = tokens[i];
    43                 // 遇到关系符"~ + > ."的时候跳出
    44 
    45                if ( Expr.relative[ (type = token.type) ] ) {
    46                     break;
    47                }
    48 
    49                // 根据type获取查询方法
    50                if ( (find = Expr.find[ type ]) ) {
    51                     // Search, expanding context for leading sibling combinators
    52                     // rsibling = /[+~]/, 用于判断同胞关系符
    53                     if ( (seed = find(
    54                          token.matches[0].replace( runescape, funescape ),
    55                          rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
    56                     )) ) {
    57 
    58                          // 如果seed是空的或者没有任何token了,就可以提前返回
    59                          // 否则,就根据新的seed和token,迭代地继续搜索下去
    60                          tokens.splice( i, 1 );
    61                          selector = seed.length && toSelector( tokens );
    62                          if ( !selector ) {
    63                               push.apply( results, seed );
    64                               return results;
    65                          }
    66                          break;
    67                     }
    68                }
    69           }
    70      }
    71       // Compile and execute a filtering function if one is not provided
    72      // Provide `match` to avoid retokenization if we modified the selector above
    73      // 执行compile返回一个匹配器函数, 再利用这个返回的函数进行匹配;
    74      ( compiled || compile( selector, match ) )(
    75           seed,
    76           context,
    77           !documentIsHTML,
    78           results,
    79           rsibling.test( selector ) && testContext( context.parentNode ) || context
    80      );
    81      return results;
    82 };
    Compile方法:
     
     1 compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
     2      var i,
     3           setMatchers = [],
     4           elementMatchers = [],
     5           cached = compilerCache[ selector + " " ];     // 根据selector获取cache中的匹配器
     6      // 如果尚未创建这个匹配器,则需要创建一个
     7      if ( !cached ) {
     8           // 产生一个函数,这个函数包含一系列递归函数用来检索每一个元素
     9           if ( !match ) {
    10                match = tokenize( selector );          // 解析选择器词法
    11           }
    12           i = match.length;
    13           while ( i-- ) {
    14                cached = matcherFromTokens( match[i] );     // 根据token创建匹配器
    15                if ( cached[ expando ] ) {
    16                     setMatchers.push( cached );
    17                } else {
    18                     elementMatchers.push( cached );
    19                }
    20           }
    21           // Cache the compiled function
    22           // matcherFromGroupMatchers 返回一个superMatcher
    23           // compelerCache = createCache(), 根据selector建立匹配器方法cache
    24           cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
    25            //在cached中保存选择器
    26           cached.selector = selector;
    27      }
    28 
    29      // 返回这个对应的匹配器
    30      return cached;
    31 };
     
    总结
    • 调用jQuery.find的时候实际上就调用了Sizzle。Sizzle的实现的一个最基本思路是以最快的速度完成选择器匹配,那如何才能够完成呢?对于简单不含其他关系符的选择器如(#id,tag,.class)就尽可能得直接调用document.getElementById/ .getElementByTagName/ .getElementByClassName 等原生方法。这些DOM的基本方法的运行速度是最快的。而对于词法更为复杂的选择器(包含关系符,伪类选择器),首选是调用document.querySelectorAll。QSA的速度非常快,但在不同的浏览器上其运行效果不一样,很多时候会有莫名奇妙的返回结果,因此使用前需要对浏览器进行测试,确保在确定能够使用QSA的情况下才进行这样的调用,否则就需要调用Sizzle.select进行更低层次的元素匹配;
    • select的实现相对而言比较复杂,它首先需要对选择器进行词法分析,然后根据所得到的词法标记利用sizzle.compile构造出一系列匹配器,并将这些匹配器组合成一个更大的匹配方法,最后才执行这个匹配方法;关于select的更详细具体的分析,留在日后再看;
    • 有select可以看出来,鉴于浏览器的兼容性问题,尤其是针对IE < 8的兼容性,jQuery在这方面作出了许多努力,使得代码的编写变得臃肿,执行效率也有所下降;
    • jQuery中使用的assert方法十分精妙,新建DOM节点元素作为测试根节点,然后进行节点匹配测试。要注意的是,测试完成后删除子节点,并对根节点赋值null,否则容易导致内存泄漏(IE);
    • jQuery的选择器是使用频率最高的方法之一,因此一切效率之上。从jQuery的源码可学习到,如何在保证运行效率的前提下保证一个方法对浏览器的兼容性。
     


  • 相关阅读:
    python内建函数
    python中fromkeys的用法
    python中append和+号的区别
    python中input用法
    opencv3.2.0之绘图
    opencv3.2.0之Mat
    【学习】Python进行数据提取的方法总结【转载】
    【python】python中的enumerate()函数【笔记】
    【学习】如何安装GraphLab Create 【转载】
    【学习】python文件读写,用with open as的好处,非常好【转载】
  • 原文地址:https://www.cnblogs.com/elcarim5efil/p/4676107.html
Copyright © 2011-2022 走看看