selector是jQuery主打的一个模块,可以说jQuery的强大就是其selector。目前其的selector已经分成另外的一个(sizzle)[]项目了。

在jQuery中采用调用sizzle来实现selector,接下来详细分析。

模块结构

jQuery的selector模块依赖如下:

selector -> selector-sizzle -> sizzle

主要实现是sizzle。selector-sizzle只是定义了一些jQuery常用的sizzle的变量。selector直接就是一个申明依赖。

Sizzle简介

Sizzle也是采用类似jQuery结构的写法,但是其并未采用模块化编写。

在官方文档中申明支持如下浏览器:

  • IE6+
  • FF3.0+
  • Chrome 5+
  • Safari 3+
  • Opera 9+

可以说主流的浏览器都主持。

sizzle听说是业界中世界最快类库。其很多方法采用原生支持的,然后按照结构优化进行解析。

sizzle常规解析是由右向左解析,例如:ul li a中sizzle是选择a,然后再a的候选集中剔除不满足li、ul的。这样做的好处是只遍历了一次文档,然后都是指针操作。

工厂方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
* Sizzle工厂方法
* @param {[type]} selector css 选择字符串
* @param {[type]} context 上下文 默认是当前文档
* @param {[type]} results 结果对象 匹配结果会添加在其中
* @param {[type]} seed 候选结果集
*/
function Sizzle( selector, context, results, seed ) {
var match, elem, m, nodeType,
// QSA vars
i, groups, old, nid, newContext, newSelector;
// 如果指定了context 那么其值为指定context的ownerDocument 否则采用preferredDoc(window.documrnt)
if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
// 设置上下文 这个方法会进行文档判断
setDocument( context );
}
context = context || document;
results = results || [];
// 选择器不是string 直接返回结果
if ( !selector || typeof selector !== "string" ) {
return results;
}
// 如果的context.nodeType不是document或者html元素名 返回空结果
if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {
return [];
}
// 是html文档 并且没有候选结果集
if ( documentIsHTML && !seed ) {
// 匹配是否是id、class或者tag 单节点
if ( (match = rquickExpr.exec( selector )) ) {
// #id的情况
if ( (m = match[1]) ) {
// 如果是document
if ( nodeType === 9 ) {
// 使用getElementById获取节点
elem = context.getElementById( m );
// 检查父节点来捕获 Blackberry 4.6 的返回结果
if ( elem && elem.parentNode ) {
// 处理当 IE、 Opera和 Webkit 等的返回结果
// 确保id属性的确是需要的
if ( elem.id === m ) {
// 增加如结果集
results.push( elem );
return results;
}
// 节点并未在文档中
} else {
// 直接返回结果
return results;
}
// 上下文是非文档
} else {
// 判断上下文是否有所属文档 如果有使用其的getElementById的方法
if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&
contains( context, elem ) && elem.id === m ) {
results.push( elem );
return results;
}
}
// tag情况
} else if ( match[2] ) {
// 直接调用context的getElementsByTagName
push.apply( results, context.getElementsByTagName( selector ) );
return results;
// .class情况
// 在支持getElementsByClassName方法时使用getElementsByClassName否则使用sizzle扩展的
} else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) {
push.apply( results, context.getElementsByClassName( m ) );
return results;
}
}
console.log('----2222---');
console.log(support.qsa);
console.log(rbuggyQSA.test( selector ));
// QSA path
// 检查是否支持querySelectorAll 并且检测是否有bug 主要是IE8/9
if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
nid = old = expando;
newContext = context;
newSelector = nodeType === 9 && selector;
// 上下文环境是tag并且是非object
// IE 8 不支持 object elements
if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
// 组合选择字符串
groups = tokenize( selector );
// 有id获取
if ( (old = context.getAttribute("id")) ) {
// 特殊字符转义
nid = old.replace( rescape, "\\$&" );
// 无id设置id
} else {
context.setAttribute( "id", nid );
}
nid = "[id='" + nid + "'] ";
i = groups.length;
while ( i-- ) {
groups[i] = nid + toSelector( groups[i] );
}
newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context;
newSelector = groups.join(",");
}
// 如果存在newSelector
if ( newSelector ) {
try {
// 直接调用原生的querySelectorAll方法
push.apply( results,
newContext.querySelectorAll( newSelector )
);
return results;
} catch(qsaError) {
} finally {
if ( !old ) {
context.removeAttribute("id");
}
}
}
}
}
// 别的情况采用select方法 <IE 10
return select( selector.replace( rtrim, "$1" ), context, results, seed );
}

根据工厂方法来分析,sizzle首先会采用原生方法判断,如果支持原生方法会直接采用原生方法实现。只有当浏览器不支持原生方法时候才采用自定义的方法(select)实现。

其中关键的是检测相关方法是否原生支持,主要有support.getElementsByClassName、support.qsa。还有就是bug列表rbuggyQSA和自定义选择方法select了。

采用querySelectorAll是否会有一个问题,比如当指定context的时候查找应该是不包含本身的,可以querySelectorAll确是包含的,以至于产生了一个问题。Sizzle采用querySelectorAll时候,如果上下文是html的tag时候,自己会构造一个id然后作为上下文然后转化为上下文为document进行查找,最后删除这个id。这样有效的排除的这个问题。

还有Sizzle是如何保证速度的呢?不难看出一个条件(id、class、tag)的时候采用原生方法getElementById、getElementsByClassName、getElementById。多条件的时候基于querySelectorAll。这些方法中有的在一些浏览器中依然有bug,Sizzle采用检测的方式来处理bug。根据bug产生情况,又可能有不同的调用。总的来说就是尽量采用原生方法,实在不行就才采用自定义的实现。

检测

Sizzle的检测都是基于一个主函数,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function assert( fn ) {
// 创建一个节点
var div = document.createElement("div");
try {
// 转换成布尔值
return !!fn( div );
} catch (e) {
return false;
} finally {
// 根据父节点来删除检测的节点
if ( div.parentNode ) {
div.parentNode.removeChild( div );
}
// IE 中得清楚内存
// IE低版本采用的垃圾是标记回收
div = null;
}
}

看出检测节点都是document下的一个节点,在检测完成后会清除。

接下来看一下getElementsByClassName的检测方法:

1
2
3
4
5
6
7
8
9
10
11
12
// 检测是否有 getElementsByClassName 的原生支持
// 并且实际验证
support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) {
div.innerHTML = "{% raw %}<div class='a'></div><div class='a i'></div>{% endraw %}";
// Safari<4
// 缓存一个类
div.firstChild.className = "i";
// Opera<10
// 会有找不到前置类的bug
return div.getElementsByClassName("i").length === 2;
});

直接使用了rnative变量(正则)来验证,然后进行实际调用验证。

qsa方法也是一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// querySelectorAll(:focus)当true时候返还false(Chrome 21)
// 在IE8/9中当iframe的`document.activeElement` 可以访问时候,
// 使用:focus时会产生错误
// 详细请看 http://bugs.jquery.com/ticket/13378
rbuggyQSA = [];// 记录querySelectorAll的bug
// 调用rnative.test判断是否支持querySelectorAll方法
// rnative是正则判断是否是原生实现的方法
if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) {
// 判断原生实现了还得进行实际验证
assert(function( div ) {
// 设置一个select的属性 赋值为空
// selected赋值bug 详细请看http://bugs.jquery.com/ticket/12359
div.innerHTML = "<select><option selected=''></option></select>";
// IE8
// 根据属性查找不出节点
if ( !div.querySelectorAll("[selected]").length ) {
// 增加一个属性选择bug
rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
}
// 在Webkit/Opera中:checked 返回的selected 是option属性
// 但是IE8中却是抛出错误
// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
if ( !div.querySelectorAll(":checked").length ) {
rbuggyQSA.push(":checked");
}
});
assert(function( div ) {
// Windows 8 Native Apps 中 使用.innerHTML时候type属性被限制
var input = doc.createElement("input");
input.setAttribute( "type", "hidden" );
div.appendChild( input ).setAttribute( "t", "" );
// 检测属性选择为空的情况
// 在常规中空属性值会直接转化属性为如:id=''->id
// 如果这样查询到值也是bug
// IE8/Opera 10-12
if ( div.querySelectorAll("[t^='']").length ) {
rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
}
// 选择:enabled
// IE8中抛异常
// FF 3.5 中隐藏元素一直是enabled
if ( !div.querySelectorAll(":enabled").length ) {
rbuggyQSA.push( ":enabled", ":disabled" );
}
// 正常中抛异常跳出函数块
// Opera 10-11中不抛伪类前带有,号的异常
div.querySelectorAll("*,:x");
rbuggyQSA.push(",.*:");
});
}
...
// querySelectorAll有bug 组合成为一个正则方便调用检测
rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );

从中看出针对很多bug进行了检测,最后形成了一个正则,现在再看工厂方法中的rbuggyQSA.test(selector)是不是感觉豁然开朗。

篇幅太长了,下一篇再接着分析吧。

参考文档