3.5 选择过滤

本节将结合下面示例继续分析当完成词法分组之后Sizzle的操作。

HTML DOM结构:

CSS选择器:

     div > p+div.sub input[type="checkbox"]

JavaScript脚本:

     <script>
     window.onload = function () {
         console.log(Sizzle('div > p+div.sub input[type="checkbox"]'))
     }
     </script>

下面按常规思维逻辑来描述主要任务。

第1步,选择div元素的所有子元素p。

第2步,选择紧邻p元素后的所有div元素,且class="sub"。

第3步,选择div.sub元素内所有input元素,且type="checkbox"。

在jQuery 3.2.1中,针对高级浏览器会自动使用querySelectorAll处理所有CSS选择器,不再使用低效的原始方法。为了深入学习Sizzle引擎,本节主要讲解在低版本中是如何实现的,其中伪类选择器、XML处理等在后文讲解,本节暂不涉及这方面的处理。

首先,读者需要了解下面知识点。

 CSS选择器的位置关系。

 CSS选择器基本实现接口。

 CSS选择器从右到左匹配原则。

3.5.1 位置关系

在HTML文档中,所有节点之间都存在如下几种关系。

 祖先和后代

 父亲和儿子

 相邻兄弟

 同级兄弟

在CSS选择器里分别对应的标识符是空格、>、+、~。

其实还有一种特殊关系—div.sub,中间没有空格表示选取一个class为sub的div节点,相当于限定关系。

在Sizzle中,专门定义了一个Expr对象,来记录选择器相关的属性及操作。它有以下属性:

在Expr.relative属性中定义了一个first属性,用来标识两个节点的“紧密”程度。例如,父子关系和相邻兄弟关系就是紧密的。在创建位置匹配器时,会根据first属性来匹配合适的节点。

3.5.2 实现接口

除了querySelector和querySelectorAll,HTML DOM提供了4个API接口。

 getElementById:上下文只能是HTML文档。

 getElementsByName:上下文只能是HTML文档。

 getElementsByTagName:上下文可以是HTML文档、XML文档、元素节点。

 getElementsByClassName:上下文可以是HTML文档、元素节点。提示,IE 8-不支持。

所以Sizzle只有三种可靠的兼容用法。

3.5.3 匹配原则

CSS选择器遵循从右到左的匹配原则。以下面CSS选择器为例:

     div > p+div.sub input[type="checkbox"]

通过词法分析器tokenize分解后,对应的规则,即分解的每个小块如下:

     type: "TAG"
     value: "div"
     matches ...

     type: ">"
     value: " > "

     type: "TAG"
     value: "p"
     matches ...

     type: "+"
     value: "+"

     type: "TAG"
     value: "div"
     matches ...

     type: "CLASS"
     value: ".sub"
     matches ...

     type: " "
     value: " "

     type: "TAG"
     value: "input"
     matches ...

     type: "ATTR"
     value: "[type="checkbox"]"
     matches ...

除关系选择器外,其余有语意的标签都对应分析出matches。例如,最后一个属性选择器分支"[type= "checkbox"]"。

分组之后,需要使用浏览器提供的API实现匹配,所以Expr.find就是最终的实现接口。

第1步,首先确定从右到左的顺序进行匹配,但是右边第一个是"[type="checkbox"]",Expr.find不认识这种选择器,所以只能往前继续找。

     type: "TAG"
     value: "input"

第2步,这种标签Expr.find能匹配到,所以就会直接调用以下代码:

由于getElementsByTagName方法返回的是一个合集,所以Sizzle在这里引入了seed(种子合集),搜索器搜到符合条件的标签,都放入这个初始集合seed中。

第3步,完成匹配之后,就不再继续往下匹配了,开始进行整理:重组CSS选择器,剔掉已经用于处理的TAG标签—input。这时CSS选择器缩减为:

     selector: "div > p+div.sub [type="checkbox"]"

如果直接剔除后,selector为空,就证明满足匹配要求,直接返回结果。

第4步,如果selector不为空,则开始进行过滤操作。这里能够使用的对象包括seed集、通过tokenize分析组成match合集。

删除input之后,CSS选择器变成:

     selector: "div > p+div.sub [type="checkbox"]"

此时,send目标合集有两个最终元素。

第5步,下面开始使用select()函数快速从两个条件中找到目标元素。select()函数的源代码如下:

这个过程比较复杂,简单总结一下:

第1步,按照从右到左原则取出最后一个token,如[type="checkbox"]。

第2步,过滤类型 如果type是>、+、~、空格四种关系选择器中的一种,则跳过,继续过滤。

第3步,直到匹配到ID、CLASS、TAG中的一种,因为这样才能通过浏览器的接口获取元素。

第4步,此时seed种子合集中就有值了,这样把匹配的范围缩小到一个很小的范围。

第5步,如果匹配的seed合集有多个,需要进一步的过滤,修正选择器selector: "div > p+div.sub [type= "checkbox"]"。

第6步,完成选择过滤之后,跳到编译函数阶段。