为了账号安全,请及时绑定邮箱和手机立即绑定

jQuery中的Sizzle引擎分析

标签:
JQuery

我分析的jQuery版本是1.8.3。Sizzle代码从3669行开始到5358行,将近2000行的代码,这个引擎的版本还是比较旧,最新的版本已经到v2.2.2了,代码已经超过2000行了。并且还有个专门的Sizzle主页。

从一个demo开始,HTML代码如下:


<div id="grand_father">    <div id="father">        <div id="child1" class="child">子集1</div>        <div id="child2" class="child">子集2</div>        <div id="child3" class="child">子集3</div>        <input type="radio" id="radio1"/>    </div></div>


然后JavaScript代码如下:

var $nodes = $('div + input[type="radio"],div.child:first-child');console.log($nodes);

1)返回的是一个jQuery对象,如下图所示,并且匹配到了两个标签,一个div和radio,

2)右边的div在0的位置,radio在1的位置,这说明jQuery的选择器匹配是从右往左的!


 

下面看一个流程图,当我编写了$('div + input[type="radio"],div.child:first-child')后发生的过程:


 

一、jQuery对象

对象是需要new一下才行的,但是jQuery只要$("xxx")后,就生成了一个对象。

1)jQuery构造函数

在第42行,将会返回一个new对象:

jQuery = function( selector, context ) {    // The jQuery object is actually just the init constructor 'enhanced'    return new jQuery.fn.init( selector, context, rootjQuery );}

 

2)jQuery对象结构

根据上面的返回对象的图中可以看到:

a. 对象的原型属性__proto__指向的是函数jQuery的原型属性prototype。__proto__ 是内部 [ [Prototype ]] ,原型链就是通过这个属性来实现的。

b. 索引是0和1的,其实是浏览器中的原生对象,我们可以搞个简单的选择器来验证,例如$("#radio1"),代码将会执行到140行


elem = document.getElementById(match[2]);// Check parentNode to catch when Blackberry 4.6 returns// nodes that are no longer in the document #6963if (elem && elem.parentNode) {  // Handle the case where IE and Opera return items  // by name instead of ID  if (elem.id !== match[2]) {    return rootjQuery.find(selector);  }  // Otherwise, we inject the element directly into the jQuery object  this.length = 1;  this[0] = elem;}this.context = document;this.selector = selector;return this;


 

二、select函数

5116行的select函数是引擎的入口:

1)在这里引用了词法分析函数tokenize。

2)当tokenize返回的Token集合数组只有一个的时候,将会寻找种子合集【通过一些原生DOM接口可获取到】,在5147行中可以看到:


/*完整的find在4089行,简易的find如下:Expr.find = {  'ID': context.getElementById,  'CLASS': context.getElementsByClassName,  'NAME': context.getElementsByName,  'TAG': context.getElementsByTagName}             */if ((find = Expr.find[type])) {  // Search, expanding context for leading sibling combinators  if ((seed = find(      token.matches[0].replace(rbackslash, ""),      rsibling.test(tokens[0].type) && context.parentNode || context,      xml    ))) {    //省略逻辑....  }}


3)通过compile编译函数,生成Token集合数组对应的匹配器,匹配后返回结果。

 

三、词法分析

高级的浏览器会直接使用querySelectorAll方法选择匹配。而低级的浏览器IE6或IE7等,就只能进入到jQuery的Sizzle引擎进行匹配。

为了调试方便,我将5182行的代码修改成“!document.querySelectorAll”,让高级浏览器也进入Sizzle引擎中匹配。

 

1)Token格式

4684行的tokenize函数最终返回的是Token集合数组,Token是一个String对象,格式如下:

String{0:'字符1',1:'字符2',....., type:'对应的Token类型【TAG,ID,CLASS,ATTR,CHILD,PSEUDO,NAME,>,+,空格,~】', matches:'正则匹配到的一个结构'}

type类型根据4150行的relative对象和4230行的filter对象中的key值获取。

 

2)返回的结果

'div + input[type="radio"],div.child:first-child'返回的数组如下:


上面返回的顺序是从左往右,先input,然后是div。

 

3)tokenize函数的流程


上图中有4个关系符号:

Expr.relative = {  ">": { dir: "parentNode", first: true },  " ": { dir: "parentNode" },  "+": { dir: "previousSibling", first: true },  "~": { dir: "previousSibling" }}

结合上面的HTML结构:

1)grand_father与child1属于祖宗与后代关系(空格表达)

2)father与child1属于父子关系,也算是祖先与后代关系(>表达)

3)child1与child2属于临近兄弟关系(+表达)

4)child1与child2,child3都属于普通兄弟关系(~表达)

 

四、编译函数

把高级规则转换成底层实现就叫编译,比如高级语言到机器语言的过程就是编译。同样把抽象的css选择语法转变成具体的匹配函数的过程也是编译。


 

1)matcherFromTokens

5080行的compile函数通过引用4931行的matcherFromTokens函数获取Token集合对应的匹配器,引用代码如下:


1 i = group.length;//从右往左2 while (i--) {3   cached = matcherFromTokens(group[i]);4   if (cached[expando]) {5     setMatchers.push(cached);6   } else {7     elementMatchers.push(cached);8   }9 }


返回了两个函数数组,对应上面的Token集合数组,由于是从右往左,所以与上面的Token集合数组反过来。【在4979行console.log(matchers)】


打开第一个值,会发现里面还嵌套着很多闭包,闭包里面又有闭包,这就是前面所说的大的匹配函数:


matcherFromTokens最后会引用4803行的elementMatcher,将上面的数组作为参数传递过去。

上面示例代码的第7行就在将函数插入到elementMatchers数组中。再传递给下面的matcherFromGroupMatchers函数。

 

2)matcherFromGroupMatchers

再引用4983行的matcherFromGroupMatchers函数生成终极匹配器,返回匹配结果。

这个函数将会return出来的一个curry化的函数,也就是4986行的superMatcher函数。

在4995行,superMatcher函数会根据参数seed 、expandContext和context确定一个起始的查询范围:

elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context )

有可能是直接从seed种子集合中获取,也有可能在context或者context的父节点范围内。

这里的context是“document”,也就是整个DOM树【在5003行console.log(elems)】,elems结构如下:


可以看出如果事先定义了content,就会把范围缩小很多,利于匹配,例如jQuery可以这样写:

$('div + input[type="radio"],div.child:first-child', $('#grand_father'))

 

在5007行开始过滤,elementMatchers参数就是上面返回的大匹配器。

之所以用for是因为选择器(div + input[type="radio"],div.child:first-child)中有“,”号,所以是两组大匹配器。


for (;(elem = elems[i]) != null; i++) {  //省略逻辑...  for (j = 0; (matcher = elementMatchers[j]); j++) {    if (matcher(elem, context, xml)) {      results.push(elem);      break;    }  }  //省略逻辑...}


 

大致过程就是这样,里面还有很多细节地方,这里就不讨论了,有兴趣的可以自己去打打断点玩玩。


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消