jQuery源码9600多行,而Sizzle引擎就独占近2000行,占了1/5。Sizzle引擎、jQuery事件机制、ajax是整个jQuery的核心,也是jQuery技术精华的体现。里面的有些策略确实很值得学习,先膜拜之,然后细细学习。

  在学习Sizzle引擎之前我们先准备一点知识,和先了解Sizzle引擎的一点工作原理。

<div id="chua">
<a>
<span>chua的测试用例</span>
</a>
<p class="group">
<label for="age">年龄:</label>
<input type="text" id="age" value="20" readonly/>
<p>
</div>

  Sizzle引擎查找节点时CSS 选择器时一定要从右往左解析?

  我们要获取for="age"的label标签,css可以这么写#chua > a + .group labe[for="age"]。

  正常流程我们是先查找#chua,然后找到直接子节点中为a标签的节点,然后在找到紧跟着的class为.group的节点,最后找到子节点为label且for属性为age的节点。可以想象当节点很多的时候,选择器引擎干了半天终于找到满足前面条件的.group节点,最后发现下面的子节点没有节点为label且for属性为age的节点,一夜回到解放前,前面的工作白做的。

  所以我们的想法就是:先看找到匹配最右边的css的节点(这一步已经缩小了很大范围),如果木有那么很好,到此结束,直接返回;如果存在,我再往左的css一个个匹配,反正在此过程中如果有不满足条件的我就踢掉。首先我们要肯定的是查找直接子节点我们要遍历所有子节点来匹配,而查找直接父节点则简单的多只有一个(能匹配的上就OK,不能匹配就丢掉),扩展到查找后代节点和祖先节点也是一样。所以从右往左查找快速得多。

  现在进入正题

  Sizzle引擎的在处理css选择器的时候有个原则:如果能使用浏览器原生的解析器来解析CSS选择器就使用之,不能的才使用Sizzle自定的解析方式来解析。可以肯定的是Sizzle引擎号称业界最快的CSS选择器解析引擎,但也快不过浏览器自带的解析器。

a.Sizzle入口,内部函数Sizzle( selector, context, results, seed )


  Sizzle函数是所有解析的入口。根据代码可知。Sizzle对一些简单情况作了处理,不需要进行select解析就直接返回结果。不能使用浏览器提供的基础函数解析的才进入select函数。

  大概的轮毂如下

function Sizzle( selector, context, results, seed ) {
  ...   if ( !selector || typeof selector !== "string" ) {return results; }
  if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {return [];}   if ( !documentIsXML && !seed ) {
    //rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/;对于简单搜索快捷解析
    if ( (match = rquickExpr.exec( selector )) ) {
      // Speed-up: Sizzle("#ID"),如果是ID查询的话查询直接返回结果
      if ( (m = match[1]) ) {
        ...
return results;
      
      // Speed-up: Sizzle("TAG"),如果是直接查询TAG,查询直接返回结果
      } else if ( match[2] ) {
        ...
        return results;

       }

      // QSA path,如果能使用浏览器高级查询querySelectorAll
      if ( support.qsa && !rbuggyQSA.test(selector) ) {
        ...
        if ( newSelector ) {
          //使用querySelectorAll能成功查询则返回结果,否则使用select
          try {
            push.apply( results, slice.call( newContext.querySelectorAll(newSelector), 0 ) );
            return results;
          } catch(qsaError) {
          } finally {
            if ( !old ) {
              context.removeAttribute("id");
            }
          }
        }
      }
    }
    // All others
    return select( selector.replace( rtrim, "$1" ), context, results, seed );
  } }

  上面的处理可以看到有三个情况是可以直接使用浏览器自带的处理的:查询ID(getElementById)、查询TAG(getElementsByTagName)、浏览器支持高级查询querySelectorAll。其他情况进入Sizzle自定义解析方式。

  我们不禁要问:为啥木有包括Class查询?说明一下,老版本的浏览器中不是所有的浏览器都支持Class查询,但是ID和TAG查询是所有浏览器都支持的。而支持使用Class查询(getElementsByClassName)的浏览器基本都支持querySelectorAll高级查询,so,不用说,用querySelectorAll即可。

  

b. 词法解析


  Sizzle引擎解析CSS选择器的第一个步骤就是要将CSS选择器分解成一个一个单独的词,这和编译原理共通。

  Sizzle的词法解析入口函数是内部函数tokenize。tokenize的作用是把CSS选择器(其实也就是一段字符串)解析成一组基础词法的序列。这个序列里面的每一个元素格式是

  Token:{

    value:'匹配到的字符串',

    type:'对应的Token类型',

    matches:'正则匹配到的一个结构'

  }

Sizzle查询结果缓存机制

  jQuery在词法解析函数tokenize开始解析之前用到了一个比较巧妙的地方,Sizzle把每次查询结果缓存了起来,如果下一次有相同的查询,则直接使用缓存中的查询结果,而不需要重新查询。

  机制的实现:

var tokenCache = createCache(),
...
function createCache() {
  var cache, keys = [];
  return (cache = function( key, value ) {
    // 使用 (key + " ")避免命名冲突,最大缓存Expr.cacheLength:50     if ( keys.push( key += " " ) > Expr.cacheLength ) {
      // Only keep the most recent entries
      delete cache[ keys.shift() ];
    }
    return (cache[ key ] = value);
  });
}
... //设置缓存
tokenCache( selector, groups ); //tokenize函数中获取缓存
cached = tokenCache[ selector + " " ]

  解析:

  createCache()返回的是一个回调函数,对于这个回调函数来说cache,和keys都是他的类全局变量,在回调函数中可以直接使用。这里的巧妙在于return (cache = function( key, value )…),将cache赋值给了tokenCache,这样使本来不能在外面使用的cache变成了tokenCache,tokenCache保存的就是最新的缓存,直接调用tokenCache[key]即可访问缓存。

  这里还有一个点就是return (cache = function( key, value )…)中cache先被赋值,然后被填充cache[key].如果是先填充cache[key],然后再赋值则cache会被新赋值覆盖。

tokenize函数详解

  tokenize函数功能是做词法分析,步骤如下

  首先,查看缓存中是否有上次同样的查询的结果,如果有则返回即可

cached = tokenCache[ selector + " " ];
if ( cached ) {
  return parseOnly ? 0 : cached.slice( 0 );
}

  然后,判断是否还有没有等待词法分析的字符串,如果没有则返回词法分析结果并将结果缓存下来;如果还有则去掉字符串前面可能存在的的逗号和空白符(比如CSS选择器为“p , span”,词法分析完p后剩下等待分析的字符串为" , span")

//如果soFar有内容则继续进行切分,知道将选择器切存储在groups中
while ( soFar ) {
  //第一次执行或者开始位置有逗号
  //rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" )
  if ( !matched || (match = rcomma.exec( soFar )) ) {
    if ( match ) {
      // 去除前面的逗号
      soFar = soFar.slice( match[0].length ) || soFar;
    }
    //在groups中新加一个数组类型的元素来保存新的tokens
    groups.push( tokens = [] );
    }
    ...
  }
}
...
// 如果仅是解析,返回剩余的字符串长度
// 否则抛出错误或返回词法解析结果 tokens
return parseOnly ?
soFar.length :
soFar ?
Sizzle.error( selector ) :
//缓存词法解析结果tokens
tokenCache( selector, groups ).slice( 0 );

  接着,在确认还有字符串需要解析的基础上,如果第一个字符是匹配父子兄弟选择器(">"/"+"/" "/"~"),将设置每一个词语的token属性压入tokens数组并将其从带解析的字符串sofar中剔除掉。

// rcombinators=/^[\x20\t\r\n\f]*([\x20\t\r\n\f>+~])[\x20\t\r\n\f]*/
//如果匹配父子兄弟选择器
if ( (match = rcombinators.exec( soFar )) ) {
  matched = match.shift();
  tokens.push( {
    value: matched,
    // Cast descendant combinators to space
    type: match[0].replace( rtrim, " " )
    } );
    soFar = soFar.slice( matched.length );
  }
}

  然后,解析其他词语。其他词语指的是下面几类ATTR/CHILD/ClASS/ID/PSEUDO/TAG,分别对应属性/子选择器/类/ID/伪类/标签  

    

  jquery有一个比较优秀的地方就是使用了很多js正则表达式,节省了很多代码,结构上也比较清晰。如下图展示的几个CSS选择器词语对应的正则  

 

  使用matchExpr[type]将每一种用到的词语匹配的结果match列出来做一下对比

["ATTR"].exec("[type='text']"):     ["[type='text']", "type", "=", "'", "text", undefined]

 ["CHILD"].exec(":nth-of-type(2)"):  [":nth-of-type(2)", "nth", "of-type", "2", "", undefined, undefined, "", "2"]

["CLASS"].exec(".chua"):            [".chua", "chua"]

["ID"].exec("#chua"):               ["#chua", "chua"]

["PSEUDO"].exec(":first"):          [":first", "first", undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

["TAG"].exec("p"):                  ["p", "p"]

  可以看出其中ATTR/CHILD/PSEUDO和另外的三种格式很不一样,为了后期便于统一处理,jQuery使用Expr.preFilter(具体的Expr.preFilter请查看源码)对这三种词语匹配的结果进行了调整,调整结果如下

match = preFilters["ATTR"]( match ):   ["[type='text']", "type", "=", "text"]
match = preFilters["CHILD"]( match ): [":nth-of-type(2)", "nth", "of-type", "2", 0, 2, undefined, "", "2"] match = preFilters["PSEUDO"]( match ): [":first", "first", undefined]

  匹配后将解析结果压入tokens中,并剔除已解析的词语

matched = match.shift();
tokens.push( {
  value: matched,
  type: type,
  matches: match
  } );
soFar = soFar.slice( matched.length );

  好了,现在我们来看一下完成词法解析后我们得到的结果吧

  以#chua > a + .group labe[for="age"]为例:

  

  如果是多组CSS选择器如:"p > .chua , #chen input"这样使用逗号分隔的,则会得到一个二维数组[tokens1,tokens2],tokens1表示两个"p > .chua"的词法解析结果,tokens2表示 "#chen input"的词法解析结果

  词法解析到此完毕。附上完整源码

function tokenize( selector, parseOnly ) {
  var matched, match, tokens, type,
    soFar, groups, preFilters,
    cached = tokenCache[ selector + " " ];
  //查缓存
  if ( cached ) {
    return parseOnly ? 0 : cached.slice( 0 );
  }   soFar = selector;
  groups = [];
  preFilters = Expr.preFilter;   //如果soFar有内容则继续进行切分,知道将选择器切存储在groups中
  while ( soFar ) {
    //第一次执行或者开始位置有逗号
    //rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" )
    if ( !matched || (match = rcomma.exec( soFar )) ) {
      if ( match ) {
        // 去除前面的逗号
        soFar = soFar.slice( match[0].length ) || soFar;
      }
      //在groups中新加一个数组类型的元素来保存新的tokens
      groups.push( tokens = [] );
    }
    matched = false;     // rcombinators=/^[\x20\t\r\n\f]*([\x20\t\r\n\f>+~])[\x20\t\r\n\f]*/
    //如果匹配父子兄弟选择器
    if ( (match = rcombinators.exec( soFar )) ) {
      matched = match.shift();
      tokens.push( {
        value: matched,
        // Cast descendant combinators to space
        type: match[0].replace( rtrim, " " )
      } );
      soFar = soFar.slice( matched.length );
    }     //filters
    ////其中Expr.filter中的过滤函数目前只在matcherFromTokens中用到
    for ( type in Expr.filter ) {
      //matchExpr包括ID, CLASS, NAME, TAG, ATTR,CHILD,PSEUDO,needsContext
      //等类型的正则表达式,matchExpr中属性的排列顺序是根据使用频率来排行的
      //节约查找时间
      //如果通过正则匹配到了Token格式:match = matchExpr[ type ].exec( soFar )
      //然后看看需不需要预处理:!preFilters[ type ]
      //如果需要 ,那么通过预处理器将匹配到的处理一下 : match = preFilters[ type ]( match )
      if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || (match = preFilters[ type ]( match ))) ) {
        matched = match.shift();
        tokens.push( {
          value: matched,
          type: type,
          matches: match
        } );
        soFar = soFar.slice( matched.length );
      }
    }
    if ( !matched ) {
      break;
    }
  }
// 如果仅是解析,返回剩余的字符串长度
// 否则抛出错误或返回词法解析结果 tokens
return parseOnly ?
soFar.length :
soFar ?
Sizzle.error( selector ) :
//缓存词法解析结果tokens
tokenCache( selector, groups ).slice( 0 );

  如果觉得本文不错,请点击右下方【推荐】!

jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——词法解析的更多相关文章

  1. mybatis源码分析(三)------------映射文件的解析

    本篇文章主要讲解映射文件的解析过程 Mapper映射文件有哪几种配置方式呢?看下面的代码: <!-- 映射文件 --> <mappers> <!-- 通过resource ...

  2. jQuery-1.9.1源码分析系列完毕目录整理

    jQuery 1.9.1源码分析已经完毕.目录如下 jQuery-1.9.1源码分析系列(一)整体架构 jQuery-1.9.1源码分析系列(一)整体架构续 jQuery-1.9.1源码分析系列(二) ...

  3. jQuery源码分析系列

    声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://git ...

  4. [转]jQuery源码分析系列

    文章转自:jQuery源码分析系列-Aaron 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAaro ...

  5. jQuery源码分析系列(转载来源Aaron.)

    声明:非本文原创文章,转载来源原文链接Aaron. 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAa ...

  6. jQuery源码分析系列——来自Aaron

    jQuery源码分析系列——来自Aaron 转载地址:http://www.cnblogs.com/aaronjs/p/3279314.html 版本截止到2013.8.24 jQuery官方发布最新 ...

  7. jquery2源码分析系列

    学习jquery的源码对于提高前端的能力很有帮助,下面的系列是我在网上看到的对jquery2的源码的分析.等有时间了好好研究下.我们知道jquery2开始就不支持IE6-8了,从jquery2的源码中 ...

  8. MyCat源码分析系列之——结果合并

    更多MyCat源码分析,请戳MyCat源码分析系列 结果合并 在SQL下发流程和前后端验证流程中介绍过,通过用户验证的后端连接绑定的NIOHandler是MySQLConnectionHandler实 ...

  9. MyCat源码分析系列之——SQL下发

    更多MyCat源码分析,请戳MyCat源码分析系列 SQL下发 SQL下发指的是MyCat将解析并改造完成的SQL语句依次发送至相应的MySQL节点(datanode)的过程,该执行过程由NonBlo ...

随机推荐

  1. 基于Picture Library创建的图片文档库中的上传多个文件功能(upload multiple files)报错怎么解决?

    复现过程 首先,我创建了一个基于Picture Library的图片文档库,名字是 Pic Lib 创建完毕后,我点击它的Upload 下拉菜单,点击Upload Picture按钮 在弹出的对话框中 ...

  2. The Swiss Army Knife of Data Structures … in C#

    "I worked up a full implementation as well but I decided that it was too complicated to post in ...

  3. SQL Server 2016 CTP2.2 的关键特性

    SQL Server 2016 CTP2.2 的关键特性 正如微软CEO 说的,SQL Server2016 是一个Breakthrough Flagship  Database(突破性的旗舰级数据库 ...

  4. 【腾讯Bugly干货分享】微信小程序开发思考总结——腾讯“信用卡还款”项目实践

    本文来自于腾讯bugly开发者社区,未经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/58212d0fa7a7574c4f4cc3c5 作者:peggy 小程序概述 1 ...

  5. [异常解决] ubuntu上安装JLink驱动遇到的坑及给后来者的建议

    一.前言 最近将整个电脑格式化,改成了linux操作系统 希望这样能让自己在一个新的世界探索技术.提升自己吧- win上的工具用多了,就不想变化了- 继上一篇<ubuntu上安装虚拟机遇到的问题 ...

  6. dofile执行ANDROID APK里面的文件

    我使用dofile执行APK文件是不行的,比如 dofile("assets/res/flist")只能先拷贝到writablePath然后再dofile拿到数据后再清除这个临时文 ...

  7. Android-找到包下面所有的类

    Android 利用反射找到包下面所有的类 Android下其实有一个DexFile的东西,利用它我们可以很好的找到包下面所有的类 什么是DexFile? 官方的说明是这样的: Manipulates ...

  8. 初识Jsp,JavaBean,Servlet以及一个简单mvc模式的登录界面

    1:JSP JSP的基本语法:指令标识page,include,taglib;page指令标识常用的属性包含Language用来定义要使用的脚本语言:contentType定义JSP字符的编码和页面响 ...

  9. [汇编与C语言关系]2. main函数与启动例程

    为什么汇编程序的入口是_start,而C程序的入口是main函数呢?以下就来解释这个问题 在<x86汇编程序基础(AT&T语法)>一文中我们汇编和链接的步骤是: $ as hell ...

  10. 动态给div中新增html

    小颖最近接触的项目中用到了    innerHTML 所以小颖今天就自己做了个demo,当当当当代码请看下方: 页面效果: