首页 > 代码库 > jQuery中的Sizzle引擎分析

jQuery中的Sizzle引擎分析

 我分析的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 #6963
if (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;
    }
  }
  //省略逻辑...
}

jQuery中的Sizzle引擎分析