我分析的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; } } //省略逻辑...}
大致过程就是这样,里面还有很多细节地方,这里就不讨论了,有兴趣的可以自己去打打断点玩玩。