作者:yanyige | 发布时间:2017-03-10 14:42
第三章 Sizzle选择器
Sizzle 是一款纯JavaScript 实现的CSS 选择器引擎,它具有以下特性:
- 完全独立,无库依赖。
- 相较于大多数常用选择器其性能非常有竞争力。
- 压缩和开启 gzip 后只有 4 KB。
- 具有高扩展性和易于使用的 API。
- 支持多种浏览器,如 IE 6.0+、Firefox 3.0+、Chrome 5+、Safari 3+、Opera 9+。
W3C Selectors API 规范定义了方法querySelector()和querySelectorAll(),它们用于根据CSS选择器规范定位文档中的元素,但是老版本的浏览器(如IE6、IE7)不支持这两个方法。在Sizzle 内部,如果浏览器支持方法querySelectorAll(),则调用该方法查找元素,如果不支持,则模拟该方法的行为。
3.1 总体结构
(function(){
// 选择器引擎入口,查找与选择器表达式 selector 匹配的元素集合
var Sizzle = function( selector, context, results, seed ) { ... };
// 工具方法,排序、去重
Sizzle.uniqueSort = function( results ) { ... };
// 便捷方法,使用指定的选择器表达式 expr 对元素集合 set 进行过滤
Sizzle.matches = function( expr, set ) { ... };
// 便捷方法,检查某个元素 node 是否匹配选择器表达式 expr
Sizzle.matchesSelector = function( node, expr ) { ... };
// 内部方法,对块表达式进行查找
Sizzle.find = function( expr, context, isXML ) { ... };
// 内部方法,用块表达式过滤元素集合
Sizzle.filter = function( expr, set, inplace, not ) { ... };
// 工具方法,抛出异常
Sizzle.error = function( msg ) { ... };
// 工具方法,获取 DOM 元素集合的文本内容
var getText = Sizzle.getText = function( elem ) { ... };
// 扩展方法和属性
var Expr = Sizzle.selectors = {
// 块表达式查找顺序
order: [ "ID", "NAME", "TAG" ],
// 正则表达式集,用于匹配和解析块表达式
match: { ID, CLASS, NAME, ATTR, TAG, CHILD, POS, PSEUDO },
leftMatch: { ... },
// 属性名修正函数集
attrMap: { "class", "for" },
// 属性值读取函数集
attrHandle: { href, type },
// 块间关系过滤函数集
relative: { "+", ">", "", "~" },
// 块表达式查找函数集
find: { ID, NAME, TAG },
// 块表达式预过滤函数集
preFilter: { CLASS, ID, TAG, CHILD, ATTR, PSEUDO, POS },
// 伪类过滤函数集
filters: { enabled, disabled, checked, selected, parent, empty, has, header,
text, radio, checkbox, file, password, submit, image, reset, button, input,
focus },
// 位置伪类过滤函数集
setFilters: { first, last, even, odd, lt, gt, nth, eq },
// 块表达式过滤函数集
filter: { PSEUDO, CHILD, ID, TAG, CLASS, ATTR, POS }
};
// 如果支持方法 querySelectorAll(),则调用该方法查找元素
if ( document.querySelectorAll ) {
(function(){
var oldSizzle = Sizzle;
Sizzle = function( query, context, extra, seed ) {
// 尝试调用方法 querySelectorAll() 查找元素
// 如果上下文是 document,则直接调用 querySelectorAll() 查找元素
return makeArray( context.querySelectorAll(query ), extra );
// 如果上下文是元素,则为选择器表达式增加上下文,然后调用querySelectorAll()
// 查找元素
return makeArray( context.querySelectorAll( "[id='" + nid + "'] " +
query ), extra );
// 如果查找失败,则仍然调用 oldSizzle()
return oldSizzle(query, context, extra, seed);
};
})();
}
// 如果支持方法 matchesSelector(),则调用该方法检查元素是否匹配选择器表达式
(function(){
var matches = html.matchesSelector
|| html.mozMatchesSelector
|| html.webkitMatchesSelector
|| html.msMatchesSelector;
// 如果支持方法 matchesSelector()
if ( matches ) {
Sizzle.matchesSelector = function( node, expr ) {
// 尝试调用方法 matchesSelector()
var ret = matches.call( node, expr );
return ret;
// 如果查找失败,则仍然调用 Sizzle()
return Sizzle(expr, null, null, [node]).length > 0;
};
}
})();
// 检测浏览器是否支持 getElementsByClassName()
(function(){
Expr.order.splice(1, 0, "CLASS");
Expr.find.CLASS = function( match, context, isXML ) { ... };
})();
// 工具方法,检测元素 a 是否包含元素 b
Sizzle.contains = function( a, b ) { ... };
})();
3.2 选择器表达式
为了准确描述Sizzle 的实现,避免歧义,需要先约定一些相关术语,具体如下所示。
序号 | 术语 | 说明和示例 —|—|— 1 | 选择器表达 | CSS 选择器表达式,例如,”div>p” 2 | 并列选择器表达 | 逗号分割的多个选择器表达式,例如,”div, p” 3 | 块表达式 | 例如,”div>p” 中的”div”、”p” 4 | 块表达式类型 | 例如,”div” 的类型是TAG,”.red” 的类型是CLASS,”div.red” 则是TAG +CLASS。共有8 种块表达式类型:ID、CLASS、NAME、ATTR、TAG、CHILD、POS、PSEUDO 5 | 块间关系符 | 表示块表达式之间关系的符号,例如,”div>p” 中的”>”。共有4 种块间关系符: “>” 父子关系、”” 祖先后代关系、”+” 紧挨着的兄弟元素、” ~ “ 之后的所有兄弟 元素
3.3 设计思路
在正式开始分析Sizzle 的源码实现之前,先来讨论和分析下如果要执行一段选择器表达式,或者说设计一个简化版的选择器引擎,需要做些什么工作。下面以”div.red>p”为例来模拟执行过程,具体来说有从左向右查找和从右向左查找两种思路:
- 1)从左向右:先查找”div.red” 匹配的元素集合,然后查找匹配”p” 的子元素集合。
- 2)从右向左:先查找”p”匹配的元素集合,然后检查其中每个元素的父元素是否匹配”div.red”。
Sizzle,它是一款从右向左查找的选择器引擎,提供了与前面3个步骤相对应的核心接口:
- 正则 chunker 负责从选择器表达式中提取块表达式和块间关系符。
- 方法 Sizzle.find( expr,context,isXML)负责查找块表达式匹配的元素集合,方法Sizzle.filter( expr, set, inplace, not ) 负责用块表达式过滤元素集合。
- 对象Sizzle.selector.relative中的块间关系过滤函数根据块间关系符过滤元素集合。函数Sizzle( selector, context, results, seed ) 则按照前面3 个步骤将这些核心接口组织起来。本节对选择器引擎和Sizzle的设计思路作了探索和概述,接下来看看Sizzle 的源码实现。
3.4 Sizzle( selector, context, results, seed )
函数Sizzle( selector, context, results, seed )用于查找与选择器表达式selector匹配的元素集合。该函数是选择器引擎的入口。
函数Sizzle( selector, context, results, seed ) 执行的6个关键步骤如下:
- 解析块表达式和块间关系符。
- 如果存在位置伪类,则从左向右查找:
-
- 查找第一个块表达式匹配的元素集合,得到第一个上下文元素集合。
-
- 遍历剩余的块表达式和块间关系符,不断缩小上下文元素集合。
- 否则从右向左查找:
-
- 查找最后一个块表达式匹配的元素集合,得到候选集、映射集。
-
- 遍历剩余的块表达式和块间关系符,对映射集执行块间关系过滤。 根据映射集筛选候选集,将最终匹配的元素放入结果集。 如果存在并列选择器表达式,则递归调用Sizzle( selector, context, results, seed )查找匹配的元素集合,并合并、排序、去重。
- 最后返回结果集。
接下来是源码实现:
1. 定义Sizzle( selector, context, results, seed )
3879 var Sizzle = function( selector, context, results, seed ) {
- 参数 selector:CSS 选择器表达式。
- 参数context:DOM元素或文档对象,作为查找元素的上下文,用于限定查找范围。默认 值 是当前文档对象。
- 参数 results:可选的数组或类数组,函数 Sizzle( selector, context, results, seed ) 将把查找到的元素添加到其中。
-
参数seed:可选的元素集合,函数Sizzle( selector, context, results, seed )将从该元素集合中过滤出匹配选择器表达式的元素集合。
2. 修正参数results、context
相关代码如下所示:
3880 results = results || [];
3881 context = context || document;
3882
3883 var origContext = context;
3884
3885 if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
3886 return [];
3887 }
3888
3889 if ( !selector || typeof selector !== "string" ) {
3890 return results;
3891 }
3892
3. 定义局部变量
3893 var m, set, checkSet, extra, ret, cur, pop, i,
3894 prune = true,
3895 contextXML = Sizzle.isXML( context ),
3896 parts = [],
3897 soFar = selector;
3898
- 变量 m:用于存放正则 chunker 每次匹配选择器表达式 selector 的结果。
- 变量 set:在从右向左的查找方式中,变量set称为“候选集”,是最后一个块表达式匹配的元素集合,其他块表达式和块间关系符则会对候选集set 进行过滤;对于从左向右的查找方式,变量set 是当前块表达式匹配的元素集合,也是下一个块表达式的上下文。
- 变量 checkSet:对于从右向左的查找方式,变量 checkSet称为“映射集”,其初始值是候选集set 的副本,其他块表达式和块间关系符则会对映射集checkSet进行过滤,过滤时先根据块间关系符将其中的元素替换为父元素、祖先元素或兄弟元素,然后把与块表达式不匹配的元素替换为false,最后根据映射集checkSet 筛选候选集set;对于从右向左的查找方式,事实上在查找过程中并不涉及变量checkSet,只是在函数Sizzle() 的最后为了统一筛选和合并匹配元素的代码,将变量checkSet 与变量set 指向了同一个数组。
- 变量extra:用于存储选择器表达式中第一个逗号之后的其他并列选择器表达式。如果存在并列选择器表达式,则会递归调用函数Sizzle( selector, context, results, seed )查找匹配元素集合,并执行合并、排序和去重操作。
- 变量 ret :只在从右向左执行方式中用到,用于存放查找器 Sizzle.find( expr,context,isXML ) 对最后一个块表达式的查找结果,格式为{ expr:“…”, set: array }。
- 变量 pop:只在从右向左的查找方式中用到,表示单个块表达式。
- 变量 prune:只在从右向左的查找方式中用到,表示候选集 set 是否需要筛选,默认为 true,表示需要筛选,如果选择器表达式中只有一个块表达式,则变量prune 为false。
- 变量 contextXML:表示上下文 context 是否是 XML 文档。
- 变量 parts:存放了正则 chunker 从选择器表达式中提取的块表达式和块间关系符。
- 变量soFar:用于保存正则chunker每次从选择器表达式中提取了块表达式或块间关系符后 的剩余部分,初始值为完整的选择器表达式。
4. 解析块表达式和块间关系符
3860 var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
3899 // Reset the position of the chunker regexp (start from head)
3900 do {
3901 chunker.exec( "" );
3902 m = chunker.exec( soFar );
3903
3904 if ( m ) {
3905 soFar = m[3];
3906
3907 parts.push( m[1] );
3908
3909 if ( m[2] ) {
3910 extra = m[3];
3911 break;
3912 }
3913 }
3914 } while ( m );
3915