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

  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 );

 

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

转载于:https://www.cnblogs.com/chuaWeb/p/jQuery-1-9-1-Sizzle-1.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 要下载 jQuery 1.9.1.min.js,你可以按照以下步骤进行操作: 首先,打开 jQuery 官方网站。在浏览器的地址栏输入"jquery.com",然后按下回车键。 点击页面上的 "Download"(下载)选项。这将会导航至 jQuery 下载页面。 在下载页面中,你可以看到一个包含多个可选择版本的列表。确保选择的是 "1.9.1" 版本。 在 "1.9.1" 版本下面,你会找到 "Download the compressed, production jQuery 1.9.1" 链接。点击该链接。 随即,一个弹出窗口将询问你是否保存或下载文件。选择 "保存文件" 选项,并选择一个合适的文件夹。 保存完毕后,你可以在选择的文件夹中找到并使用 "jquery-1.9.1.min.js" 文件。 现在,你已成功下载了 jQuery 1.9.1.min.js 文件,你可以在你的项目中引入该文件并开始使用 jQuery 的功能了。 ### 回答2: 要下载 jQuery 1.9.1 版本的压缩文件 `jquery-1.9.1.min.js`,可以通过以下步骤进行操作: 1. 首先,在浏览器中打开 jQuery 官方网站(https://jquery.com/)。 2. 在网站的顶部导航菜单中,找到 "Download"(下载)选项,点击进入下载页面。 3. 在下载页面中,你会看到多个版本的 jQuery 可供选择。找到并选择 "1.x" 系列版本,然后点击该系列下的 "Download the compressed, production jQuery 1.9.1"(下载已压缩的 jQuery 1.9.1)链接。 4. 开始下载后,浏览器会提示保存文件的位置和名称。你可以选择保存到自己想要的目录,并将文件名设置为 "jquery-1.9.1.min.js"。 5. 确认保存位置和文件名后,点击 "保存",文件下载就会开始。 下载完成后,你就可以在你选择的目录中找到 "jquery-1.9.1.min.js" 文件了。这个文件是已经压缩过的 jQuery 1.9.1 版本的 JavaScript 文件,你可以将其引入到你的网页中使用 jQuery 的功能。 ### 回答3: jQuery-1.9.1.min.js 是一个 JavaScript 库,用于简化网页开发中常见的操作和任务。这个文件可以通过多种方式进行下载。 首先,可以通过 jQuery 官方网站(https://jquery.com/download/)来下载 jQuery-1.9.1.min.js 文件。在网站上,你可以找到各种不同版本的 jQuery 文件,包括被压缩(minified)的版本和未压缩的版本。选择需要的版本后,点击下载按钮即可下载。 如果你使用的是包管理工具,比如 npm 或者 Yarn,你也可以通过命令行界面来下载 jQuery 文件。打开命令行界面,进入你的项目目录,然后运行适当的命令来下载文件。例如,在使用 npm 的情况下,可以运行命令:```npm install jquery@1.9.1``` 。这样可以将 jQuery-1.9.1.min.js 文件下载到你的项目目录下的 node_modules 文件夹中。 此外,你还可以从第方网站下载 jQuery-1.9.1.min.js 文件。有很多网站提供 jQuery 文件的下载,包括一些开发工具和资源网站。通过搜索引擎搜索 "jQuery-1.9.1.min.js 下载",你可以找到很多下载链接。请注意,从第方网站下载文件时,务必选择可信赖的来源,以避免下载到不安全或被修改过的文件。 总而言之,下载 jQuery-1.9.1.min.js 文件的方式有多种,你可以通过 jQuery 官网、包管理工具或者第方网站来获取该文件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值