IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    chua1989发表于 2015-11-10 09:56:00
    love 0

      Sizzle引擎的主体部分已经分析完毕了,今天为这部分划一个句号。

    a. Sizzle解析流程总结


      是时候该做一个总结了。Sizzle解析的流程已经一目了然了。

      1.选择器进入Sizzle( selector, context, results, seed )函数,先对选择器不符合要求的(比如没有选择器、选择器不为字符串、上下文环境context不是节点元素且不是document)排除,直接返回;接着判断如果选择器是简单的选择器(只有一个元选择器,比如“#ID”、“.CLASS”、“TAG”等)直接用浏览器支持的函数获取结果返回。接着判断如果浏览器支持高级搜索querySelectorAll且选择器也符合参数标准则获取结果返回。否则进入select( selector, context, results, seed )函数接着处理。

      2.进入select( selector, context, results, seed )函数后。select先对选择器selector进行词法解析(tokenize( selector, parseOnly )函数),在没有备选种子seed且选择器并非多个并列选择器(“div >p,input”等用逗号分隔的是并列选择器)的情况下缩小选择范围,包括对原子选择器组tokens的第一个元选择器(最小的选择器)token = tokens[0]为“#ID”且id的下一个token是关系节点(">"/"+"/" "/"~")的情况先通过Expr.find["ID"]缩小范围(context = Expr.find["ID"](…));剩下的tokens的第一给token不是(">"/"+"/"~")的任何一个且tokens中没有Sizzle自定义的位置伪类(位置伪类有:first、last、even、odd、eq(n)、gt(n)、lt(n))的情况下,找到最后一个元选择器且保证该元选择器后面没有任何关系选择器,如果找到则根据元选择器查找到备选seed(

    seed = find(token.matches[0].replace( runescape, funescape ), rsibling.test( tokens[0].type ) && context.parentNode || context)

    ),此时如果seed为空或者tokens中没有选择器了则直接返回结果,否则拿着缩小范围的seed继续往下查。进入compile ( selector, group /* Internal Use Only */ )编译好的函数中继续。

      3. compile ( selector, group /* Internal Use Only */ )函数最终将返回一个执行终极匹配函数的curry化函数。该curry化函数最终在select函数中执行

    compile( selector, match )( seed, context, documentIsXML, results, rsibling.test( selector ))

    ; compile函数实际就做了两件事情。一件是用tokens生成一个终极匹配函数(cached = matcherFromTokens( group[i] ))压入setMatchers或elementMatchers中。另一件事就是生成并返回执行终极匹配函数的curry化函数(

    cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );

    )

      4.使用matcherFromTokens生成tokens对应的终极匹配函数。matchers数组用来保存所有匹配函数数组。matchers初始化时有一个默认的匹配函数。从左往右遍历选择器(0-n遍历tokens)如果遇到关系选择器(matcher = Expr.relative[ tokens[i].type ]),则整合关系选择器和原来的matchers生成一个新的匹配函数(matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];);如果遇到非关系选择器,获取选择器匹配函数(matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );),分两种情况处理,一种是伪类选择器则将选择器tokens分成四个部分分别生成相应的匹配函数然后整合直接返回终极匹配函数(

    return setMatcher(
      i
    > 1 && elementMatcher( matchers ),
      i
    > 1 && toSelector( tokens.slice( 0, i - 1 ) ).replace( rtrim, "$1" ),
      matcher,
      i
    < j && matcherFromTokens( tokens.slice( i, j ) ),//如果伪类后面紧跟伪类并列选择器(比如“:first.chua span”)中的".chua"
      j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),//如果伪类(或伪类并列选择器)还有选择器要筛选   
      j
    < len && toSelector( tokens )
      );

    ),另一种是非伪类选择器则将元选择器匹配函数压入matchers中(matchers.push( matcher );)。然后通过elementMatcher( matchers )生成终极匹配函数返回。

      5.使用matcherFromGroupMatchers( elementMatchers, setMatchers )返回一个执行终极匹配函数的curry化函数。这个curry化函数内部流程是:先确定起始查找范围,或是seed,或是整个树节点。遍历终极匹配器集合elementMatchers,如果元素匹配则将节点放入结果集中

    while ( (matcher = elementMatchers[j++]) ) {
      if ( matcher( elem, context, xml ) ) {
        results.push( elem );
        break;
      }
    }

    。然后遍历有伪类选择器的终极匹配函数做相应的处理

    while ( (matcher = setMatchers[j++]) ) {
      matcher( unmatched, setMatched, context, xml );
    }

    。完成所有流程后把匹配结果返回。

           到此基本流程就结束啦。可能内部有些小细节没有深究明白,但是流程已经OK了。希望大家有所收获,哈哈。

     

    b. 选择器效率问题


       一般来说浏览器原生函数效率getElementById > getElementsByName >= getElementsByClassName。

      测试案例(借用Arron的测试案例)

    <div id="demo" class='demo'>
      <ul>
        <li><input type="checkbox" data="data"/></li>
        <li></li >
        <li> </li>
        <li></li >
      </ul>
    </div >

      测试方法(定时10秒看执行次数)

    <script type="text/javascript">
    var dateStart,dateEnd, count = 0;

    //这是其中一个案例
    function testID(){
    dateStart
    = new Date();
    count
    = 0;

    while(1){
    dateEnd
    = new Date();
    if(dateEnd - dateStart > 10000){
    console.log(
    '#demo li:nth-child(1) run count = ' + count);
    break;
    }

    $(
    '#demo li:nth-child(1)');
    count
    ++;
    }
    }

    testID();</script>

    第一组测试:连续书写、分段书写、使用find方式比较

      测试出三种jQuery写法在10秒钟执行的次数:

      $('#demo li:nth-child(1)') :        #demo li:nth-child(1) run count = 1186316

      $('li:nth-child(1)','#demo') :          li:nth-child(1) ,#demo run count = 537820

      $('#demo').find('li:nth-child(1)') :   #demo find li:nth-child(1) run count = 562195

     

      晕了,怎么和Arron测试的结果不同:arron的测试结果在点击这里查看

      不过我根据jQuery的源码推测一下:

      分两种情况:

      第一种,支持querySelectorAll下

      $('#demo li:nth-child(1)') 方式Sizzle会先直接使用querySelectorAll查询'#demo li:nth-child(1)'

      $('li:nth-child(1)','#demo')方式先$('#demo')得到结果作为context进入Sizzle使用querySelectorAll查询'li:nth-child(1)'。比第一种多走一步

      $('.demo').find('li:nth-child(1)')方式和$('li:nth-child(1)','#demo')类似,效率应该差不多。

      第二种,不支持querySelectorAll的情况下(我用IE7测试)

      日志: #demo li:nth-child(1) run count = 51781
      日志: li:nth-child(1),#demo run count = 51982
      日志: (#demo).find(li:nth-child(1)) run count = 52831

      他们之间没有太大的差距。分析Sizzle流程可知,三种方式都是先计算出"#demo",然后匹配" li:nth-child(1)"。

      所以按照分析,在现代浏览器(IE8+,chrome,fireforx)的这三种写法中连写是最快的。

      

      再测试一组数据

      $('.demo li:nth-child(1)') :             .demo li:nth-child(1) run count = 1201026

      $('li:nth-child(1)','.demo') :             li:nth-child(1) ,.demo run count = 409030

      $('.demo').find('li:nth-child(1)') :.demo find li:nth-child(1) run count = 417080

      结果还是连写最快。

      

      我换另外的一种方法来测试上面的实例(循环次数一定的情况下比较时长):

    //其中一个测试用例
    function
    testID(){
    dateStart
    = new Date()
    count
    = 0;
    while(1){
    if(count > 1000000){
    break;
    }

    $(
    '.demo li:nth-child(1)');
    count
    ++;
    }
    dateEnd
    = new Date();
    console.log(
    '.demo li:nth-child(1) run time = ' + (dateEnd - dateStart));
    }

      看看结果:

      #demo li:nth-child(1) run time = 11519

      li:nth-child(1),#demo run time = 23556
      (#demo).find(li:nth-child(1)) run time = 22016

     

      .demo li:nth-child(1) run time = 8076

      li:nth-child(1),.demo run time = 20218
      (.demo).find(li:nth-child(1)) run time = 20045

      还是连写花费的时间最少

      也证明我第一种测试方式(定时10秒比较执行次数)测试用例是没有问题的!还以为是我弄错了呢,至于Arron的测试结果为什么和我不同我就不知道了。

      所以按照分析,在现代浏览器(IE8+,chrome,fireforx)的这三种写法中连写是最快的。 究其原因是保持了从右往左解析的优势。

     

    第二组:CSS属性选择器、CSS伪类选择器比较

     

      $('.demo li > input[type="checkbox"]') : run count = 424106

      $('.demo li > input:checkbox') :      run count = 266172

      结论是CSS属性选择器和CSS伪类选择器处于同一个量级,但是CSS属性选择器会略快,但也没有快到超出一个量级。在现代浏览器何以使用高级搜索querySelectorAll()来查询的情况下当然是原生的比较快。jQuery自定义的伪类比较慢。

     

    第三组:CSS约束

      $('.demo > ul li:nth-child(1) input[type="checkbox"]') : run count = 355359

      $('.demo input[type="checkbox"]') :           run count = 403864

      避免过度约束

     

    第四组:CSS筛选伪类、避免筛选伪类

      $('.demo li > input:eq(0)')    run count = 36565

      $('.demo li > input:first')          run count = 36224

     

      $('.demo li > input').eq(0)  run count = 359582

      可见,伪类是拖慢选择器效率的一大罪魁祸首

      

     

    总结:

    1. 连续方式书写jQuery更快
    2. 多用#ID选择器,总是以#ID选择器打头缩小选择范围
    3. 让选择器最右边的选择器具有特征性,如“#id input”可换成“#id input[type=’text’]”。因为比较是从右往左开始的,右边的选择器能够确定唯一结果更好。
    4. 尽量使用高级浏览器原生css选择器替换jQuery自定义伪类。
    5. 避免过度约束和冗余约束。
    6. 使用亲密关系的关系选择器“>”和“+”,避免使用“ ”和“~”,可以避免递归匹配。
    7. 尽量避免使用伪类。
    8. 缓存jQuery对象,然后在缓存对象上使用查找。

     

      如果觉得本文还有那么一点点作用,请顶一下。


    本文链接:jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——总结与性能分析,转载请注明。



沪ICP备19023445号-2号
友情链接