Math.pow(1.1, Day)

Sizzle浅析-1

Sizzle是jQuery的御用选择器引擎,号称业界最高效的选择器。废话不说,一起来窥探Sizzle的内部结构。

试想,我们自己造一个选择器,常规思维可能是根据选择器表达式从左往右逐步缩小上下文范围访问进行查询,最后返回符合条件的结果集。在Sizzle中,打破常规思维,反其道而行之,对部分表达式从右向左查询过滤。从而极大提升了查询效率。下面的内容会一步步分析Sizzle的处理逻辑。

文章用到的相关知识点和变量解释:

  • css选择器: id, class, name, tag, 属性选择器(E[att=val]), 后代选择器(E F), 子级选择器(E > F), 相邻兄弟选择器(E + F), 通用兄弟选择器(E ~ F), 群组选择器(E, F),伪类选择器(E:first-child)。Sizzle在各类CSS选择器的基础上新增了部分选择器,然后归纳成八种供使用者调用,即:id, class, name, tag, attr(属性选择), child(子级选择器), pos(位置选择器), pseudo(伪类选择器)。
  • selector: 选择器表达式,如: #wrap li a.hover
  • chunker: 是一段正则表达式,Sizzle通过它来将selector分割成块表达式(子表达式)
  • parts: 存放chunker分割后的块表达式数组
  • context: 当前查询的上下文
  • find: 查询器,对块表达式进行查找,返回的DOM元素叫做候选集
  • set: 候选集(待筛选的集合)
  • checkSet: 映射集,候选集的副本。执行自身过滤(filter)和关系过滤(relative)时是基于映射集操作
  • result: 结果集
  • seed: 种子集
  • filter: 自身过滤器
  • relative: 关系过滤器

###Sizzle工作流程

从简单入手,比如查询’div li a.on’ ,如何实现?

  1. 首先需要将选择器表达式分割成可供查询的块表达式,即:”div”, “li”, “a.on”
  2. 然后通过查询器查询单个块表达式(先且不说是从左往右还是从右往左查询),如果还有残留表达式(比如div#id执行id查找后还余下div),则调用过滤器过滤表达式,得到候选集
  3. 接下来在候选集中查询或者过滤其他块表达式
  4. 上一步,直至表达式为空。最后返回结果集。

再看两个表达式: div li a:first div li a:first-child 两种选择器表达式的结果如何? 根据Sizzle定义和测试结果可知两种表达式的结果是不一样的: ‘div li a:first’等价于(div li a):first,位置选择器的参照物是其左侧所有块表达式的结果集。而’div li a:first-child’等价于div li (a:first-child),子级选择器的参照物是a标签自身,不依赖前面的块选择器的查询结果。在Sizzle中对含有位置选择器的表达式,采用从左往右,逐步缩小上下文的正向查询方式;反之,则采用从右往左的逆向过滤策略。这将在后面的源码分析中有体现。

通过上面的分析过程,可以把Sizzle的整体架构粗略分为三大块:

  1. chunker正则分割选择器
  2. 查找 Sizzle.find
  3. 过滤 Sizzle.filter(自身过滤),Expr.relative (关系过滤)。

###Sizzle整体代码架构 上面只是粗略分类,细看Sizzle,会发现内部的架构还是有点复杂的,为了便于分析源码,先用一张图来描述内部结构。

Sizzle-structure

如图,find和filter只是一个入口,具体逻辑都放在Expr(Sizzle.selectors)中来实现:Expr.find, Expr.filter。这样做的好处是,将来升级时,只需要修改底层实现即可,和入口耦合的方法不会受影响。Expr对象中定义了大部分核心的方法和属性:

  • order 定义使用何种选择器查询的优先级
  • match 包含了各种选择器的正则(备注1)。
  • leftMatch 在修正的match基础上增加捕获块表达式前的字符串的正则
  • relative 该对象下包含了四种选择器的过滤方法,即”+”(相邻兄弟选择器), “>”(子级选择器), ““(后代选择器), “~”(通用兄弟选择器)。其中,对后代选择器(““)和通用兄弟选择器(“~”)又调用dirNodeCheck/dirCheck方法继续过滤。
  • find Sizzle.find的核心处理器,包括id,name,tag原生查找方式。在后面的代码中,如果检测到浏览器支持getElementsByClassName,则将class方法也加入到find处理器中。
  • preFilter 预过滤器,Sizzle.filter执行过滤时会先调用preFilter进行预过滤,对表达式做一些修正,便于filter后续过滤。有个特例,如果是class选择器,则直接在预过滤中执行过滤操作。
  • filters: 伪类过滤处理器
  • setFilters: 位置过滤处理器
  • filter: Sizzle.filter的核心处理器,负责处理大部分过滤。对于位置过滤和伪类过滤,则交给上面提到的filters和setFilters去处理

备注: Sizzle在后面的代码中会对match修正来加强校验,表达式后面禁止直接跟随”]”、”)”字符)

Expr.match[type] = new RegExp(Expr.match[type].source + (/(?![^\[]*\])(?![^\(]*\))/.source));

在阅读源码或文章时,若感觉函数调用有点头大时,希望这张图能帮你理清头绪。