读Sizzle的源码,分析的Sizzle版本号是2.3.3
。
浏览器原生支持的元素查询方法:
方法名 | 方法描述 | 兼容性描述 |
---|---|---|
getElementById | 根据元素ID查询元素 | IE6+, Firefox 2+, Chrome 4+, Safari 3.1+ |
getElementsByTagName | 根据元素名称查询元素 | IE6+, Firefox 2+, Chrome 4+, Safari 3.1+ |
getElementsByClassName | 根据元素的class查询元素 | IE9+, Firefox 3+, Chrome 4+, Safari 3.1+ |
getElementsByName | 根据元素name属性查询元素 | IE10+(IE10以下不支持或不完善), FireFox23+, Chrome 29+, Safari 6+ |
querySelector | 根据选择器查询元素 | IE9+(IE8部分支持), Firefox 3.5+, Chrome 4+, Safari 3.1+ |
querySelectorAll | 根据选择器查询元素 | IE9+(IE8部分支持), Firefox 3.5+, Chrome 4+, Safari 3.1+ |
在Sizzle中,出于性能考虑,优先考虑使用JS的原生方法进行查询。上面列出的方法中,除了querySelector
方法没有被用到,其它都在Sizzle中有使用。
对于不可以使用原生方法直接获取结果的case,Sizzle就需要进行词法分析,分解这个复杂的CSS选择器,然后再逐项查询过滤,获取最终符合查询条件的元素。
有以下几个点是为了提高这种低级别查询的速度:
- 从右至左: 传统的选择器是从左至右,比如对于选择器
#box .cls a
,它的查询过程是先找到id=box
的元素,然后在这个元素后代节点里查找class中包含cls
元素;找到后,再查找这个元素下的所有a
元素。查找完成后再回到上一层,继续查找下一个.cls
元素,如此往复,直至完成。这样的做法有一个问题,就是有很多不符合条件元素,在查找也会被遍历到。而对于从右向左的顺序,它是先找到所有a
的元素,然后在根据剩下的选择器#box .cls
,筛选出符合这个条件的a
元素。这样一来,等于是限定了查询范围,相对而言速度当然会更快。但是需要明确的一点是,并不是所有的选择器都适合这种从右至左的方式查询。也并不是所有的从右至左查询都比从左至右快,只是它覆盖了绝大多数的查询情况。 - 限定种子集合: 如果只有一组选择器,也就是不存在逗号分隔查询条件的情况;则先查找最末级的节点,在最末级的节点集合中筛选;
- 限定查询范围: 如果父级节点只是一个ID且不包含其它限制条件,则将查询范围缩小到父级节点;
#box a
; - 缓存特定数据 : 主要分三类,tokenCache, compileCache, classCache;
我们对Sizzle的查询分为两类:
- 简易流程(没有位置伪类)
- 带位置伪类的查询
简易流程
简易流程在进行查询时,遵循 从右至左的流程。
梳理一下简易流程
简易流程忽略的东西主要是和位置伪类相关的处理逻辑,比如:nth-child之类的
词法分析
词法分析,将字符串的选择器,解析成一系列的TOKEN。
首先明确一下TOKEN的概念,TOKEN可以看做最小的原子,不可再拆分。在CSS选择器中,TOKEN的表现形式一般是TAG、ID、CLASS、ATTR等。一个复杂的CSS选择器,经过词法分析后,会生成一系列的TOKEN,然后根据这些Token进行最终的查询和筛选。
下面举个例子说明一下词法分析的过程。对于字符串#box .cls a
的解析:
/**
* 下面是Sizzle中词法解析方法 tokennize 的核心代码 1670 ~ 1681 行
* soFar = '#box .cls a'
* Expr.filter 是Sizzle进行元素过滤的方法集合
* Object.getOwnPropertyNames(Expr.filter) // ["TAG", "CLASS", "ATTR", "CHILD", "PSEUDO", "ID"]
*/
for ( type in Expr.filter ) {
// 拿当前的选择字符串soFar 取匹配filter的类型,如果能匹配到,则将当前的匹配对象取出,并当做一个Token存储起来
// matchExpr中存储一些列正则,这些正则用于验证当前选择字符串是否满足某一token语法
if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
(match = preFilters[ type ]( match ))) ) {
matched = match.shift();
tokens.push({
value: matched,
type: type,
matches: match
});
// 截取掉匹配到选择字符串,继续匹配剩余的字符串(继续匹配是通过这段代码外围的while(soFar)循环实现的)
// matchExpr中存储的正则都是元字符“^”开头,验证字符串是否以‘xxx’开头;这也就是说, 词法分析的过程是从字符串开始位置,从左至右,一下一下地剥离出token
soFar = soFar.slice( matched.length );
}
}
经过上述的解析过程后,#box .cls a
会被解析成如下形式的数组:
Sizzle: tokens
编译函数
编译函数的流程很简单,首先根据sel