博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
jQuery 源码分析 7: sizzle
阅读量:4679 次
发布时间:2019-06-09

本文共 12936 字,大约阅读时间需要 43 分钟。

  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 = "" + 5           ""; 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,利用快捷方式并设置context15           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根元素直接返回results27                 // Precompiled matchers will still verify ancestry, so step up a level28                } 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 matching35           // matchExpr["needsContext"]测试选择器是否含有位置伪类,如:first,:even,或包含"> + ~"等关系36 37           // 如果包含将i赋值0,否则赋值tokens.length38           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 combinators52                     // rsibling = /[+~]/, 用于判断同胞关系符53                     if ( (seed = find(54                          token.matches[0].replace( runescape, funescape ),55                          rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context56                     )) ) {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 provided72      // Provide `match` to avoid retokenization if we modified the selector above73      // 执行compile返回一个匹配器函数, 再利用这个返回的函数进行匹配;74      ( compiled || compile( selector, match ) )(75           seed,76           context,77           !documentIsHTML,78           results,79           rsibling.test( selector ) && testContext( context.parentNode ) || context80      );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 function22           // matcherFromGroupMatchers 返回一个superMatcher23           // compelerCache = createCache(), 根据selector建立匹配器方法cache24           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的源码可学习到,如何在保证运行效率的前提下保证一个方法对浏览器的兼容性。
 

转载于:https://www.cnblogs.com/elcarim5efil/p/4676107.html

你可能感兴趣的文章
什么是 PWA
查看>>
Single Number II
查看>>
hostapd源代码分析(三):管理帧的收发和处理
查看>>
java---Unicode-字符转换器
查看>>
客户端的测试
查看>>
Server Tomcat v7.0 Server at localhost failed to start.
查看>>
js数据结构之列表的详细实现方法
查看>>
【转】eclipse 安装插件
查看>>
js基础学习之-js全局对象
查看>>
解决【win10管理员已阻止程序运行】问题时有感
查看>>
NumPy切片和索引
查看>>
Linux CentOS7 VMware 文件和目录权限chmod、更改所有者和所属组chown、umask、隐藏权限lsattr/chattr...
查看>>
易语言拖拽文件命令,拖放对象组件应用
查看>>
MySQL同步故障:" Slave_SQL_Running:No" 两种解决办法 (转载)
查看>>
Activiti系列——如何在eclipse中安装 Activiti Designer插件
查看>>
Linux系统下,启动Tomcat有时报Address already in use
查看>>
读苹果开发文档时遇到瓶颈,转而花2天看了Objc基本语法
查看>>
Freemark List 倒序输出
查看>>
数据库被黑后留下的数据
查看>>
来创个博客分享学习一下
查看>>