jQuery中的Sizzle引擎分析

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

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

复制代码

<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')后发生的过程:ide

 

1、jQuery对象

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

1)jQuery构造函数this

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

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

 

2)jQuery对象结构prototype

根据上面的返回对象的图中能够看到:

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;

复制代码

 

2、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集合数组对应的匹配器,匹配后返回结果。

 

3、词法分析

高级的浏览器会直接使用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都属于普通兄弟关系(~表达)

 

4、编译函数

把高级规则转换成底层实现就叫编译,好比高级语言到机器语言的过程就是编译。一样把抽象的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;
    }
  }  //省略逻辑...}

复制代码

 

大体过程就是这样,里面还有不少细节地方,这里就不讨论了,有兴趣的能够本身去打打断点玩玩。

相关文章
相关标签/搜索