前言
上篇文章《jQuery源码剖析(八)——Sizzle选择器引擎之解析原理》谈到一个终极匹配器superMatcher,它把候选元素集合seed筛选出最后符合规则的节点集合。如下图:
从这个图中我们知道,superMatcher的输入是DOM节点结合,输出也是DOM节点集合。这个终极匹配器里边需要一堆小的匹配器来判断当前输入的节点是否符合规则。
因此容易想到这样的匹配器的形式如:function(elem){return true;}
也即是说,输入一个DOM节点,判断一下输入节点是否符合某个匹配规则,如果符合了,那它就应该在最后的结果集里边。
Sizzle中的元匹配器
可以把“元”理解为“原子”,也就是最小的那个匹配器。每条选择器规则最小的几个单元可以划分为:ATTR | CHILD | CLASS | ID | PSEUDO | TAG
在Sizzle里边有一些工厂方法用来生成对应的这些元匹配器,它就是Expr.filter。
举2个例子(ID类型的匹配器由Expr.filter["ID"]生成,应该是判断elem的id属性跟目标属性是否一致),源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | //ID元匹配器工厂 Expr.filter["ID"] = function( id ) { var attrId = id.replace( runescape, funescape ); //生成一个匹配器, return function( elem ) { var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); //去除节点的id,判断跟目标是否一致 return node && node.value === attrId; }; }; //属性元匹配器工厂 //name :属性名 //operator :操作符 //check : 要检查的值 //例如选择器 [type="checkbox"]中,name="type" operator="=" check="checkbox" Expr.filter["ATTR"] = function( name, operator, check ) { //返回一个元匹配器 return function( elem ) { //先取出节点对应的属性值 var result = Sizzle.attr( elem, name ); //看看属性值有木有! if ( result == null ) { //如果操作符是不等号,返回真,因为当前属性为空 是不等于任何值的 return operator === "!="; } //如果没有操作符,那就直接通过规则了 if ( !operator ) { return true; } //转成字符串 result += ""; return //如果是等号,判断目标值跟当前属性值相等是否为真 operator === "=" ? result === check : //如果是不等号,判断目标值跟当前属性值不相等是否为真 operator === "!=" ? result !== check : //如果是起始相等,判断目标值是否在当前属性值的头部 operator === "^=" ? check && result.indexOf( check ) === 0 : //这样解释: lang*=en 匹配这样 <html lang="xxxxenxxx">的节点 operator === "*=" ? check && result.indexOf( check ) > -1 : //如果是末尾相等,判断目标值是否在当前属性值的末尾 operator === "$=" ? check && result.slice( -check.length ) === check : //这样解释: lang~=en 匹配这样 <html lang="zh_CN en">的节点 operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : //这样解释: lang=|en 匹配这样 <html lang="en-US">的节点 operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : //其他情况的操作符号表示不匹配 false; }; }, |
其他的元匹配器工厂在这里就不贴源码了。详细源码见:https://github.com/jquery/sizzle
Sizzle中的匹配器生成原理
先上图后剖析源码:
上述的的流程说明了,Sizzle会将一个规则的Token序列转换成一个elementMatcher匹配器,然后把Seed集合里边的DOM节点用elementMatcher匹配器进行过滤,最后得到符合规则的结果集。
elementMatcher匹配器的匹配算法是从右到左的,分别从matcher1顺序匹配到matcher8。
上图的流程是,先用matcher1匹配看看当前节点是否为div节点。然后前边遇到了一个空格,因此会寻找当前匹配到的节点的祖宗节点,看看是否匹配matcher3匹配器。接着拿着这个匹配到的“祖先”节点再进去匹配matcher4,matcher5,matcher6,剩下的流程就省略不展开叙述了。
Sizzle中的匹配器源码
(注意:这里的源码有做过适当修改,主要是为了阐述方便。)
看回源码实现。首先,我们需要将Token的每个子规则生成元匹配器matcher,得到一个匹配器集合matchers:
1 2 3 4 5 6 7 8 9 10 11 | for (i = start; i < len; i++ ) { if ( (matcher = Expr.relative[ tokens[i].type ]) ) {//如果是位置词素:>+~空格,一会解释这个addCombinator //可以看到会先把之前的匹配器集合合并成一个elementMatcher //例如上图中matcher4,matcher5,matcher6合并成matcher3 matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; } else {//其他情况调用Expr.filter的工厂方法来生成匹配器 matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); matchers.push( matcher ); } } return elementMatcher( matchers ); |
最后通过elementMatcher方法来把这对匹配器合并成一个终极匹配器,当然上篇文章介绍过选择器的匹配是从右到左的,所以:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function elementMatcher( matchers ) { //生成一个终极匹配器 return matchers.length > 1 ? //如果是多个匹配器的情况,那么就需要elem符合全部匹配器规则 function( elem, context, xml ) { var i = matchers.length; //从右到左开始匹配 while ( i-- ) { //如果有一个没匹配中,那就说明该节点elem不符合规则 if ( !matchers[i]( elem, context, xml ) ) { return false; } } return true; } : //单个匹配器的话就返回自己即可 matchers[0]; } |
如果是这类没有位置词素的选择器:’#id.clr[name="checkbox"]‘,从右到左依次看看当前节点elem是否匹配规则即可。但是由于有了位置词素,那么判断的时候就不是简单判断当前节点了,可能需要判断elem的兄弟或者父亲节点是否依次符合规则。这是一个递归深搜的过程。addCombinator方法就是为了生成有位置词素的匹配器。
记得上篇文章《jQuery源码剖析(八)——Sizzle选择器引擎之解析原理》中的“CSS选择器的位置信息”一节,位置词素有以下几种,first属性代表两个规则的“紧密程度”(例如父子是紧密的,祖宗是不紧密的)
1 2 3 4 5 6 | Expr.relative = { ">": { dir: "parentNode", first: true }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: true }, "~": { dir: "previousSibling" } } |
于是根据这个词素信息就可以生成有位置词素信息的匹配器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | //matcher为当前词素前的“终极匹配器” //combinator为位置词素 function addCombinator( matcher, combinator, base ) { var dir = combinator.dir, checkNonElements = base && dir === "parentNode", doneName = done++; return combinator.first ? // Check against closest ancestor/preceding element //如果是紧密关系的位置词素 function( elem, context, xml ) { while ( (elem = elem[ dir ]) ) { if ( elem.nodeType === 1 || checkNonElements ) { //找到第一个亲密的节点,立马就用终极匹配器判断这个节点是否符合前面的规则 return matcher( elem, context, xml ); } } } : // Check against all ancestor/preceding elements //如果是不紧密关系的位置词素 function( elem, context, xml ) { var data, cache, outerCache, dirkey = dirruns + " " + doneName; while ( (elem = elem[ dir ]) ) { //以下代码相比源码有所删减 if ( elem.nodeType === 1 || checkNonElements ) { //如果是不紧密的位置关系 //那么一直匹配到true为止 //例如祖宗关系的话,就一直找父亲节点直到有一个祖先节点符合规则为止 if ( matcher( elem, context, xml ) === true ) { return true; } } } }; } |
Sizzle基本的匹配器原理大概已经分析完毕了,现在,Sizzle就剩下一个位置伪类的匹配器以及匹配过程没分析了,下一篇把Sizzle最后这一部分剖析一下。
本文链接:jQuery源码剖析(九)——Sizzle选择器引擎之匹配器
转载声明:本博客文章若无特别说明,皆为原创,转载请注明来源:拉风的博客,谢谢!
jQuery源码剖析(十)——Sizzle选择器引擎之位置伪类 | raphealguo'blog
[...] raphealguo'blog 跳至正文 首页Works笔记资源旅游With娜娜关于我 ← jQuery源码剖析(九)——Sizzle选择器引擎之匹配器 [...]