经过词法解析之后做什么?
遍历解析好的Token序列
去生成一个个匹配器,在Expr.filter
中已经提前写好了全部的TAG
、ID
、CLASS
、ATTR
、CHILD
、PSEUDO
、NAME
7种匹配器。然后将DOM元素挨个与生成的匹配器匹配,如果所有匹配器都返回true,那该DOM元素就是我们需要的DOM元素。
预编译
其实Sizzle源码的处理和上面说的有些不一样,上面说的是将DOM元素挨个匹配,其实在源码中不是这样的。源码中是遍历Token序列
生成对应的元匹配器,然后将多个元匹配器融合成一个超级匹配器,这样就能用一个方法完成匹配。
// 预编译
Sizzle.compile = function(selector, match) {
var
i,
elementMatchers = [],
cached;
i = match.length;
while(i--) {
cached = matcherFromTokens(match[i]);
elementMatchers.push(cached);
}
return matcherFromGroupMatchers(elementMatchers);
}
这是我简化过的Sizzle.compile()
方法,源码中很多的干扰项都被我除掉了。这里面有两个重要的方法:matcherFromTokens
和matcherFromGroupMatchers
。
matcherFromTokens
生成用于匹配单个选择器群组的函数
function matcherFromTokens(tokens) {
var
i,
token,
matcher,
matchers = [];
for (i = 0; i < tokens.length; i++) {
token = tokens[i];
if (matcher = Expr.relative[token.type]) { // 生成层级匹配器
matchers = [addCombinator(elementMatcher(matchers), matcher)];
} else {
// 问题:为什么这里不写成matcher = Expr.filter[token.type](token.matches);
// 因为ATTR类型的匹配器需要传入三个参数
matcher = Expr.filter[token.type].apply(null, token.matches);
matchers.push(matcher);
}
}
return elementMatcher(matchers);
}
像div > p + .aaron[type="checkbox"], #id:first-child
这个选择器中有两组tokens,分别将div > p + .aaron[type="checkbox"]
和#id:first-child
传入matcherFromTokens中进行编译,这也是为什么Sizzle.compile()
方法中要使用while循环去遍历match的原因。
编译无非是将token取出来生成元匹配器,然后放入到matchers中,最后通过elementMatcher()
方法将所有匹配器整合成一个方法并输出。生成匹配器什么的都很简单啦,因为在Expr.filter
中已经写好了,直接拿过来用即可。真正难的是处理层级关系>, +, 空格, ~
,Sizzle源码是通过addCombinator()
方法去处理的。
还有一个细节就是tokens的遍历方式是从左到右,为什么是这样自己脑内模拟程序执行流程或许就能明白。这东西太过抽象无法用语言描述,只能自行体会。
elementMatcher
这个方法很简单,就是将匹配器集合遍历挨个执行
function elementMatcher( matchers ) {
return matchers.length > 1 ?
// 如果是多个匹配器
function( elem, context, xml ) {
var i = matchers.length;
while ( i-- ) {
if ( !matchers[ i ]( elem, context, xml ) ) {
return false;
}
}
return true;
} :
// 如果是单个匹配器,返回自己即可
matchers[ 0 ];
}
注意这里遍历顺序又是从右往左
addCombinator
这个方法也很难去讲解,只有自行调试才能理解
// 层级关系处理器
function addCombinator(matcher, combinator) {
var dir = combinator.dir;
return combinator.first ?
// 紧密型
function( elem, context, xml ) {
while ( ( elem = elem[ dir ] ) ) {
// 加这么一个判断是因为有的时候拿到的是text类型的元素
if ( elem.nodeType === 1 ) {
return matcher( elem, context, xml );
}
}
return false;
} :
// 非紧密型一直递归查询,查到最后为null会跳出循环并且返回false
// 如果递归过程中某一层返回true
function( elem, context, xml ) {
while ( ( elem = elem[ dir ] ) ) {
if ( elem.nodeType === 1 ) {
if (matcher(elem, context, xml)) {
return true;
}
}
}
return false;
}
}
matcherFromGroupMatchers
这个方法很容易理解,选择器div > p + .aaron[type="checkbox"], #id:first-child
会生成两组matcherFromTokens匹配器,将DOM元素分别匹配两个匹配器,只要有一个匹配器返回为true,就将DOM元素保存,最后一起返回。
// 超级匹配器(多组)
// 像div > p + .aaron[type="checkbox"], #id:first-child这种选择器有两组matcherFromTokens匹配器
// 该方法的作用就是将两组matcherFromTokens匹配器合并成一个匹配器
function matcherFromGroupMatchers(elementMatchers) {
return function (seed, results) {
var i,
j,
elems, // 种子合集
elem, // 种子合集中的单个DOM元素
matcher; // matcherFromTokens匹配器
// 如果没有种子元素则全文检索
elems = seed || Expr.find["TAG"]("*");
// 遍历所有元素
for (i = 0; i < elems.length; i++) {
j = 0;
elem = elems[i];
// 遍历所有matcherFromTokens匹配器
// 把elems中的元素挨个放入elementMatchers匹配器中
// 若返回为true,将该元素放入results集合中
while (matcher = elementMatchers[j++]) {
if (matcher(elem)) {
results.push(elem);
}
}
}
}
}
最后
Sizzle.select = function(selector) {
var
seed, // 种子集合:类型为ID、TAG、CLASS中任意一种的DOM元素集合
match, // 词法解析后的数据
results = [] // 目标元素
;
// 词法解析
match = Sizzle.tokenize(selector);
// 预编译
var superMatcher = Sizzle.compile(selector, match);
superMatcher(seed, results);
return results;
}
总结
整个Sizzle源码学下来,我只学会了如何使用闭包。可能有人觉得只学会了一个东西也太废了,先听我把话说完。我觉得整个Sizzle源码最有价值的部分就是预编译(Sizzle.compile
)部分,elementMatcher
是预编译里面最重要的方法,说白了它不过就是个while
循环,但它和普通while
循环不一样的地方是它用闭包
的技巧将被循环的对象保存下来,保证后续调用的时候能够取出来。
至于其他的如何词法解析、如何筛选seed合集、匹配器怎么写的都显得不那么重要了。闭包
这个概念我也不是第一次见了,以前在书上看到过很多次关于它的讲解,但是我从来都没学会怎么去使用。通过这次的源码学习,我能够亲眼看见别人是如何去玩转闭包
这个东西的。果然学习光靠看书是没用的,实践才是检验真理的唯一标准。
在此献上源码
传送门