jQuery 2.0.3 源码分析Sizzle引擎 - 解析原理

声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢!

先来回答博友的提问:

如何解析

  1. div > p + div.aaron input[type="checkbox"]

顺便在深入理解下解析的原理:

HTML结构

  1. <div id="text">
  2. <p>
  3. <input type="text" />
  4. </p>
  5. <div class="aaron">
  6. <input type="checkbox" name="readme" value="Submit" />
  7. <p>Sizzle</p>
  8. </div>
  9. </div>

选择器语句

  1. div > p + div.aaron input[type="checkbox"]

组合后的意思大概就是:

1. 选择父元素为 <div> 元素的所有子元素 <p> 元素

2. 选择紧接在 <p> 元素之后的所有 <div> 并且class="aaron " 的所有元素

3. 之后选择 div.aaron 元素内部的所有 input并且带有 type="checkbox" 的元素

就针对这个简单的结构,我们实际中是不可能这么写的,但是这里我用简单的结构,描述出复杂的处理

我们用组合语句,jquery中,在高级浏览器上都是用过querySelectorAll处理的,所以我们讨论的都是在低版本上的实现,伪类选择器,XML 要放到后最后,本文暂不涉及这方便的处理.

需要用到的几个知识点:

1: CSS选择器的位置关系

2: CSS的浏览器实现的基本接口

3: CSS选择器从右到左扫描匹配


CSS选择器的位置关系

文档中的所有节点之间都存在这样或者那样的关系

其实不难发现,一个节点跟另一个节点有以下几种关系:

祖宗和后代

父亲和儿子

临近兄弟

普通兄弟

在CSS选择器里边分别是用:空格;>;+;~

(其实还有一种关系:div.aaron,中间没有空格表示了选取一个class为aaron的div节点)

  1. <div id="grandfather">
  2. <div id="father">
  3. <div id="child1"></div>
  4. <div id="child2"></div>
  5. <div id="child3"></div>
  6. </div>
  7. </div>
  • 爷爷grandfather与孙子child1属于祖宗与后代关系(空格表达)
  • 父亲father与儿子child1属于父子关系,也算是祖先与后代关系(>表达)
  • 哥哥child1与弟弟child2属于临近兄弟关系(+表达)
  • 哥哥child1与弟弟child2,弟弟child3都属于普通兄弟关系(~表达)

在Sizzle里有一个对象是记录跟选择器相关的属性以及操作:Expr。它有以下属性:

  1. relative = {
  2. ">": { dir: "parentNode", first: true },
  3. " ": { dir: "parentNode" },
  4. "+": { dir: "previousSibling", first: true },
  5. "~": { dir: "previousSibling" }
  6. }

所以在Expr.relative里边定义了一个first属性,用来标识两个节点的“紧密”程度,例如父子关系和临近兄弟关系就是紧密的。在创建位置匹配器时,会根据first属性来匹配合适的节点。


CSS的浏览器实现的基本接口

除去querySelector,querySelectorAll

HTML文档一共有这么四个API:

  • getElementById,上下文只能是HTML文档。
  • getElementsByName,上下文只能是HTML文档。
  • getElementsByTagName,上下文可以是HTML文档,XML文档及元素节点。
  • getElementsByClassName,上下文可以是HTML文档及元素节点。IE8还没有支持。

所以要兼容的话sizzle最终只会有三种完全靠谱的可用

  1. Expr.find = {
  2. 'ID' : context.getElementById,
  3. 'CLASS' : context.getElementsByClassName,
  4. 'TAG' : context.getElementsByTagName
  5. }

CSS选择器从右到左扫描匹配

接下我们就开始分析解析规则了

1. 选择器语句

  1. div > p + div.aaron input[type="checkbox"]

2. 开始通过词法分析器tokenize分解对应的规则(这个上一章具体分析过了)

  1. 分解每一个小块
  2. type: "TAG"
  3. value: "div"
  4. matches ....
  5.  
  6. type: ">"
  7. value: " > "
  8.  
  9. type: "TAG"
  10. value: "p"
  11. matches ....
  12.  
  13. type: "+"
  14. value: " + "
  15.  
  16. type: "TAG"
  17. value: "div"
  18. matches ....
  19.  
  20. type: "CLASS"
  21. value: ".aaron"
  22. matches ....
  23.  
  24. type: " "
  25. value: " "
  26.  
  27. type: "TAG"
  28. value: "input"
  29. matches ....
  30.  
  31. type: "ATTR"
  32. value: "[type="checkbox"]"
  33. matches ....
  34.  
  35. 除去关系选择器,其余的有语意的标签都都对应这分析出matches
  36.  
  37. 比如
  38. 最后一个属性选择器分支
  39. "[type="checkbox"]"
  40.  
  41. matches = [
  42. 0: "type"
  43. 1: "="
  44. 2: "checkbox"
  45. ]
  46. type: "ATTR"
  47. value: "[type="checkbox"]"

所以就分解出了9个部分了

那么如何匹配才是最有效的方式?

3. 从右往左匹配

最终还是通过浏览器提供的API实现的, 所以Expr.find就是最终的实现接口了

首先确定的肯定是从右边往左边匹配,但是右边第一个是

  1. "[type="checkbox"]"

很明显Expr.find 中不认识这种选择器,所以只能在往前扒一个

趴到了

  1. type: "TAG"
  2. value: "input"

这种标签Expr.find能匹配到了,所以直接调用

  1. Expr.find["TAG"] = support.getElementsByTagName ?
  2. function(tag, context) {
  3. if (typeof context.getElementsByTagName !== strundefined) {
  4. return context.getElementsByTagName(tag);
  5. }
  6. } :

但是getElementsByTagName方法返回的是一个合集

所以

这里引入了seed - 种子合集(搜索器搜到符合条件的标签),放入到这个初始集合seed中

OK了 这里暂停了,不在往下匹配了,在用这样的方式往下匹配效率就慢了


开始整理:

重组一下选择器,剔掉已经在用于处理的tag标签,input

所以选择器变成了:

  1. selector: "div > p + div.aaron [type="checkbox"]"

这里可以优化下,如果直接剔除后,为空了,就证明满足了匹配要求,直接返回结果了

到这一步为止

我们能够使用的东东:

1 seed合集

2 通过tokenize分析解析规则组成match合集

  1. 本来是9个规则快,因为匹配input,所以要对应的也要踢掉一个所以就是8个了

3 选择器语句,对应的踢掉了input

  1. "div > p + div.aaron [type="checkbox"]"

此时send目标合集有2个最终元素了

那么如何用最简单,最有效率的方式从2个条件中找到目标呢?

涉及的源码:

  1. //引擎的主要入口函数
  2. function select(selector, context, results, seed) {
  3. var i, tokens, token, type, find,
  4. //解析出词法格式
  5. match = tokenize(selector);
  6.  
  7. if (!seed) { //如果外界没有指定初始集合seed了。
  8. // Try to minimize operations if there is only one group
  9. // 没有多组的情况下
  10. // 如果只是单个选择器的情况,也即是没有逗号的情况:div, p,可以特殊优化一下
  11. if (match.length === 1) {
  12.  
  13. // Take a shortcut and set the context if the root selector is an ID
  14. tokens = match[0] = match[0].slice(0); //取出选择器Token序列
  15.  
  16. //如果第一个是selector是id我们可以设置context快速查找
  17. if (tokens.length > 2 && (token = tokens[0]).type === "ID" &&
  18. support.getById && context.nodeType === 9 && documentIsHTML &&
  19. Expr.relative[tokens[1].type]) {
  20.  
  21. context = (Expr.find["ID"](token.matches[0].replace(runescape, funescape), context) || [])[0];
  22. if (!context) {
  23. //如果context这个元素(selector第一个id选择器)都不存在就不用查找了
  24. return results;
  25. }
  26. //去掉第一个id选择器
  27. selector = selector.slice(tokens.shift().value.length);
  28. }
  29.  
  30. // Fetch a seed set for right-to-left matching
  31. //其中: "needsContext"= new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
  32. //即是表示如果没有一些结构伪类,这些是需要用另一种方式过滤,在之后文章再详细剖析。
  33. //那么就从最后一条规则开始,先找出seed集合
  34. i = matchExpr["needsContext"].test(selector) ? 0 : tokens.length;
  35.  
  36. //从右向左边查询
  37. while (i--) { //从后开始向前找!
  38. token = tokens[i]; //找到后边的规则
  39.  
  40. // Abort if we hit a combinator
  41. // 如果遇到了关系选择器中止
  42. //
  43. // > + ~ 空
  44. //
  45. if (Expr.relative[(type = token.type)]) {
  46. break;
  47. }
  48.  
  49. /*
  50. 先看看有没有搜索器find,搜索器就是浏览器一些原生的取DOM接口,简单的表述就是以下对象了
  51. Expr.find = {
  52. 'ID' : context.getElementById,
  53. 'CLASS' : context.getElementsByClassName,
  54. 'NAME' : context.getElementsByName,
  55. 'TAG' : context.getElementsByTagName
  56. }
  57. */
  58. //如果是:first-child这类伪类就没有对应的搜索器了,此时会向前提取前一条规则token
  59. if ((find = Expr.find[type])) {
  60.  
  61. // Search, expanding context for leading sibling combinators
  62. // 尝试一下能否通过这个搜索器搜到符合条件的初始集合seed
  63. if ((seed = find(
  64. token.matches[0].replace(runescape, funescape),
  65. rsibling.test(tokens[0].type) && context.parentNode || context
  66. ))) {
  67.  
  68. //如果真的搜到了
  69. // If seed is empty or no tokens remain, we can return early
  70. //把最后一条规则去除掉
  71. tokens.splice(i, 1);
  72. selector = seed.length && toSelector(tokens);
  73.  
  74. //看看当前剩余的选择器是否为空
  75. if (!selector) {
  76. //是的话,提前返回结果了。
  77. push.apply(results, seed);
  78. return results;
  79. }
  80.  
  81. //已经找到了符合条件的seed集合,此时前边还有其他规则,跳出去
  82. break;
  83. }
  84. }
  85. }
  86. }
  87. }
  88.  
  89. // "div > p + div.aaron [type="checkbox"]"
  90.  
  91. // Compile and execute a filtering function
  92. // Provide `match` to avoid retokenization if we modified the selector above
  93. // 交由compile来生成一个称为终极匹配器
  94. // 通过这个匹配器过滤seed,把符合条件的结果放到results里边
  95. //
  96. // //生成编译函数
  97. // var superMatcher = compile( selector, match )
  98. //
  99. // //执行
  100. // superMatcher(seed,context,!documentIsHTML,results,rsibling.test( selector ))
  101. //
  102. compile(selector, match)(
  103. seed,
  104. context, !documentIsHTML,
  105. results,
  106. rsibling.test(selector)
  107. );
  108. return results;
  109. }

这个过程在简单总结一下:

  1. selector"div > p + div.aaron input[type="checkbox"]"
  2.  
  3. 解析规则:
  4. 1 按照从右到左
  5. 2 取出最后一个token 比如[type="checkbox"]
  6. {
  7. matches : Array[3]
  8. type : "ATTR"
  9. value : "[type="
  10. checkbox "]"
  11. }
  12. 3 过滤类型 如果type > + ~ 四种关系选择器中的一种,则跳过,在继续过滤
  13. 4 直到匹配到为 ID,CLASS,TAG 中一种 , 因为这样才能通过浏览器的接口索取
  14. 5 此时seed种子合集中就有值了,这样把刷选的条件给缩的很小了
  15. 6 如果匹配的seed的合集有多个就需要进一步的过滤了,修正选择器 selector: "div > p + div.aaron [type="checkbox"]"
  16. 7 OK,跳到一下阶段的编译函数

Sizzle不仅仅是简简单单的从右往左匹配的

Sizzle1.8开始引入编译函数的概念,也是下一章的重点

jQuery 2.0.3 源码分析Sizzle引擎解析原理的更多相关文章

  1. jQuery 2.0.3 源码分析Sizzle引擎 - 解析原理

    声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 先来回答博友的提问: 如何解析 div > p + div.aaron input[type="checkb ...

  2. jQuery 2.0.3 源码分析Sizzle引擎 - 编译函数(大篇幅)

    声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 从Sizzle1.8开始,这是Sizzle的分界线了,引入了编译函数机制 网上基本没有资料细说这个东东的,sizzle引入这 ...

  3. jQuery 2.0.3 源码分析Sizzle引擎 - 高效查询

    为什么Sizzle很高效? 首先,从处理流程上理解,它总是先使用最高效的原生方法来做处理 HTML文档一共有这么四个API: getElementById 上下文只能是HTML文档 浏览器支持情况:I ...

  4. jQuery 2.0.3 源码分析Sizzle引擎 - 词法解析

    声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 浏览器从下载文档到显示页面的过程是个复杂的过程,这里包含了重绘和重排.各家浏览器引擎的工作原理略有差别,但也有一定规则. 简 ...

  5. jQuery 2.0.3 源码分析Sizzle引擎 - 超级匹配

    声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 通过Expr.find[ type ]我们找出选择器最右边的最终seed种子合集 通过Sizzle.compile函数编译器 ...

  6. jQuery 2.0.3 源码分析 Deferred(最细的实现剖析,带图)

    Deferred的概念请看第一篇 http://www.cnblogs.com/aaronjs/p/3348569.html ******************构建Deferred对象时候的流程图* ...

  7. jQuery 2.0.3 源码分析core - 选择器

         声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢!      打开jQuery源码,一眼看去到处都充斥着正则表达式,jQuery框架的基础就是查询了,查询文档元素对象 ...

  8. jQuery 2.0.3 源码分析 Deferred概念

    JavaScript编程几乎总是伴随着异步操作,传统的异步操作会在操作完成之后,使用回调函数传回结果,而回调函数中则包含了后续的工作.这也是造成异步编程困难的主要原因:我们一直习惯于“线性”地编写代码 ...

  9. jQuery 2.0.3 源码分析 事件绑定 - bind/live/delegate/on

    事件(Event)是JavaScript应用跳动的心脏,通过使用JavaScript ,你可以监听特定事件的发生,并规定让某些事件发生以对这些事件做出响应 事件的基础就不重复讲解了,本来是定位源码分析 ...

随机推荐

  1. java_部署jar

    javaw -ms100m -mx256m -jar MyApp.jar 上面指定了使用最小100M最大256M内存. 4)如果main函数要带参数 java -mss10m -mx300m -jar ...

  2. android 数据共享

    android数据共享的各种部件中的应用是最重要的3途径: 第一.使用Application子类来实现数据共享. 例如,请看下面的例子: /**  * @author YangQuanqing 特征: ...

  3. .NET平台机器学习

    .NET平台机器学习资源汇总,有你想要的么? 接触机器学习1年多了,由于只会用C#堆代码,所以只关注.NET平台的资源,一边积累,一边收集,一边学习,所以在本站第101篇博客到来之际,分享给大家.部分 ...

  4. 安裝 Rails 開發環境

    安裝 Rails 開發環境 Give someone a program, you frustrate them for a day; teach them how to program, you f ...

  5. TCP连接状态

    TCP 连接状态按 TCP 协议的标准表示法, TCP 可具有如下几种状态,为讨论方便,如下讨论中区分服务端和客户端,实际软件处理上对二者一视同仁. CLOSED关闭状态.在两个通信端使用“三路握手” ...

  6. js+css3动态时钟-------Day66

    昨天,有一天招,宽带到底没装上.相当恼火,不过包了一天租新房,有很多想法下来,其中,率先实现了--动态时钟(它已上载的资源,一些粗略的全貌.汗...) 这里记录.这个看似简单的功能,以达到良好的,我在 ...

  7. JDK动态代理机制

    JDK Proxy OverView jdk的动态代理是基于接口的.必须实现了某一个或多个随意接口才干够被代理.并且仅仅有这些接口中的方法会被代理. 看了一下jdk带的动态代理api,发现没有样例实在 ...

  8. 个人学习JQ插件编写成果:little酷炫的图片滑动切换效果

    工作一个多月了,好久没来冒冒泡了,看了@wayong的JQ插件教程,自己编写了一个模仿拉勾网首页广告栏滑动特效的JQ插件,现在跟朋友们分享分享! 先上demo链接:http://runjs.cn/de ...

  9. Cts分析框架(4)-添加任务

    Debug watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaXRmb290YmFsbA==/font/5a6L5L2T/fontsize/400/fill/ ...

  10. php连接sql server 2008数据库

    原文:php连接sql server 2008数据库 关于php连接sql server 2008的问题,2000的版本可以直接通过php中的配置文件修改,2005以上的版本就不行了,需要使用微软公司 ...