前言
这篇文章的主题是解释Sizzle选择器引擎中的解析原理,以及部分源码注释。注意,这篇文章会做一下两点限制:
- 暂时忽略位置选择器的伪类,例如,:even, :eq等
- 暂时忽略XML格式,应该说之后的剖析都不会关心jQuery如何解析XML,文中贴出的源码会把XML那部分相关的去除掉
我接下去的几篇文章会用以下例子来逐步剖析Sizzle的解析原理以及匹配器构成原理:
1 2 3 4 5 6 7 8 9 10 11 | <div> <p> <span class="clr"> <input type="text" /> </span> </p> <div class="clr"> <input type="checkbox" name="readme" /> <p>Hello World</p> </div> </div> |
使用以下选择器S来选取节点:$(‘div .clr > input[name="readme"] + p’)。
在开始前,我们必须了解一个真相:CSS选择器的解析顺序是从右到左的。
CSS选择器的解析顺序
选择器的直观意思是从左到右的,例如选择器S的表达就是从左到右的,在”div节点”底下的”class为clr的节点”的孩子节点”input节点”并且其属性name为readme,……
但是如果解析器用这种从左到右的方式来解析会有什么问题?
一般来说,HTML文档生成的DOM节点数是比较多的,而你书写的选择器selector绝大多数只能匹配到DOM树中较少的节点(否则就是你规则写得太不好了,一次性选择太多DOM节点会影响效率),因此需要有一种算法来快速判断出selector不匹配当前节点。
假设用从左到右的顺序解析:’div .clr > input[name="readme"] + p’,我们需要先找到所有div节点,然后从第一个div节点开始向下找class=”clr”的节点,一直深度下去,遇到不匹配的情况,就必须回溯到一开始搜索的div节点,然后去搜索下个div节点,重复这样的过程。这样的搜索过程对于一个只是匹配很少节点的选择器来说,效率是极低的,因为我们花费了大量的时间在回溯匹配不符合规则的节点。
如果换个思路,我们一开始过滤出跟目标节点最符合的集合出来,再在这个集合进行搜索,大大降低了搜索空间,这思路很赞:从右到左来解析选择器!
还是上边那个选择器,一开始我们把DOM树中所有的p节点找出来,紧接着我们判断这些节点中的前兄弟节点是否符合input[name="readme"]这个规则,这样就又减少了集合的元素,只有符合当前的子规则才会匹配再上一条子规则。
浏览器解析CSS的引擎就是用这样的算法去解析,同理,Sizzle引擎也是如此,并且在源码里边,它判断一个节点是否符合某个规则的行为定义为matcher。
如果你还感兴趣,建议这篇文章《How browsers work》,里边有更详细的解释。
CSS选择器的位置信息
其实不难发现,一个节点跟另一个节点有以下几种关系:祖宗和后代、父亲和儿子、临近兄弟、普通兄弟。在CSS选择器里边分别是用:空格;>;+;~
(其实还有一种关系:div.clr,中间没有空格表示了选取一个class为clr的div节点)在Sizzle里有一个对象是记录跟选择器相关的属性以及操作:Expr。它有以下属性:
1 2 3 4 5 6 | Expr.relative = { ">": { dir: "parentNode", first: true }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: true }, "~": { dir: "previousSibling" } } |
里边的first属性就是代表了两个关系的“紧密”程度。
1 2 3 4 5 6 7 | <div id="grandfather"> <div id="father"> <div id="child1"></div> <div id="child2"></div> <div id="child3"></div> </div> </div> |
- 爷爷grandfather与孙子child1属于祖宗与后代关系(空格表达)
- 父亲father与儿子child1属于父子关系,也算是祖先与后代关系(>表达)
- 哥哥child1与弟弟child2属于临近兄弟关系(+表达)
- 哥哥child1与弟弟child2,弟弟child3都属于普通兄弟关系(~表达)
所以在Expr.relative里边定义了一个first属性,用来标识两个节点的“紧密”程度,例如父子关系和临近兄弟关系就是紧密的。在创建位置匹配器时,会根据first属性来匹配合适的节点。
Sizzle引擎的解析过程
先上流程图:
前面已经说到CSS选择器解析时是按照从右到左的顺序分析,因此第一步是先取出最右的规则,如果说我们能够通过最右的规则先确定一个基本符合条件的集合那效率肯定是最好的。
如果确定不了,那就只能把整个DOM树节点拿出来作为初始集合了,在Sizzle里边,这个集合命名为seed。Sizzle里边搜索开始的入口是select函数。
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | function Sizzle( selector, context, results, seed ) { /* 这里一坨代码就不粘出来了 */ //如果是单条规则的选择器,能通过直接原生接口:getElementById|getElementsByTagName|getElementsByClassName,得到的,那就直接调原生接口 //如果是多条规则的情况,先看看浏览器有没有原生的querySelectorAll接口 // All others //如果是低级浏览器,那没办法了,只能自己处理了。 return select( selector.replace( rtrim, "$1" ), context, results, seed ); } //引擎的主要入口函数 function select( selector, context, results, seed ) { var i, tokens, token, type, find, match = tokenize( selector );//记得上篇文章《jQuery源码剖析(七)——Sizzle选择器引擎之词法分析》吗 if ( !seed ) {//如果外界没有指定初始集合seed了。 // Try to minimize operations if there is only one group if ( match.length === 1 ) {//如果只是单个选择器的情况,也即是没有逗号的情况:div, p,可以特殊优化一下 // Take a shortcut and set the context if the root selector is an ID tokens = match[0] = match[0].slice( 0 );//取出选择器Token序列 if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && context.nodeType === 9 && !documentIsXML && Expr.relative[ tokens[1].type ] ) { context = Expr.find["ID"]( token.matches[0].replace( runescape, funescape ), context )[0]; if ( !context ) { return results; } selector = selector.slice( tokens.shift().value.length ); } // Fetch a seed set for right-to-left matching //其中: "needsContext"= new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) //即是表示如果没有一些结构伪类,这些是需要用另一种方式过滤,在之后文章再详细剖析。 //那么就从最后一条规则开始,先找出seed集合 i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; while ( i-- ) {//从后开始向前找! token = tokens[i];//找到后边的规则 // Abort if we hit a combinator //如果遇到位置规则类型,那就比较郁闷了,只能是整个DOM树节点全部去过滤扫描了 if ( Expr.relative[ (type = token.type) ] ) { break; } /* 先看看有没有搜索器find,搜索器就是浏览器一些原生的取DOM接口,简单的表述就是以下对象了 Expr.find = { 'ID' : context.getElementById, 'CLASS' : context.getElementsByClassName, 'NAME' : context.getElementsByName, 'TAG' : context.getElementsByTagName } */ //如果是:first-child这类伪类就没有对应的搜索器了,此时会向前提取前一条规则token if ( (find = Expr.find[ type ]) ) { // Search, expanding context for leading sibling combinators //尝试一下能否通过这个搜索器搜到符合条件的初始集合seed if ( (seed = find( token.matches[0].replace( runescape, funescape ), rsibling.test( tokens[0].type ) && context.parentNode || context )) ) { //如果真的搜到了 // If seed is empty or no tokens remain, we can return early //把最后一条规则去除掉 tokens.splice( i, 1 ); selector = seed.length && toSelector( tokens ); //看看当前剩余的选择器是否为空 if ( !selector ) { //是的话,提前返回结果了。 push.apply( results, slice.call( seed, 0 ) ); return results; } //已经找到了符合条件的seed集合,此时前边还有其他规则,跳出去 break; } } } } } // Compile and execute a filtering function // Provide `match` to avoid retokenization if we modified the selector above // 交由compile来生成一个称为终极匹配器 // 通过这个匹配器过滤seed,把符合条件的结果放到results里边 compile( selector, match )( seed, context, documentIsXML, results, rsibling.test( selector ) ); return results; } |
而compile函数呢是通过传递进来的selector和match生成匹配器:
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 | compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { var i, setMatchers = [], elementMatchers = [], cached = compilerCache[ selector + " " ]; if ( !cached ) {//依旧看看有没有缓存 // Generate a function of recursive functions that can be used to check each element if ( !group ) { group = tokenize( selector ); } i = group.length;//从后开始生成匹配器 while ( i-- ) { //这里用matcherFromTokens来生成对应Token的匹配器 cached = matcherFromTokens( group[i] ); if ( cached[ expando ] ) {//什么是setMatchers,这里以后再剖析 setMatchers.push( cached ); } else {//普通的那些匹配器都压入了elementMatchers里边 elementMatchers.push( cached ); } } // Cache the compiled function //这里可以看到,是通过matcherFromGroupMatchers这个函数来生成最终的匹配器 cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); } //把这个终极匹配器返回到select函数中 return cached; }; |
而在matcherFromGroupMatchers里边,返回的是一个终极匹配器superMatcher(重点关注一下第10行到41行):
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | superMatcher = function( seed, context, xml, results, expandContext ) { var elem, j, matcher, setMatched = [], matchedCount = 0, i = "0", unmatched = seed && [], outermost = expandContext != null, contextBackup = outermostContext, // We must always have either seed elements or context //这一步很关键! //如果说有初始集合seed,那用它 //如果没有,那只能把整个DOM树节点取出来过滤了,可以看出选择器最右边应该写一个 elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), // Use integer dirruns iff this is the outermost matcher dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); //这里的outermost,不了解是做啥用途的,请看懂的同志们告知一下。 if ( outermost ) { outermostContext = context !== document && context; cachedruns = matcherCachedRuns; } // Add elements passing elementMatchers directly to results // Keep `i` a string if there are no elements so `matchedCount` will be "00" below //好,开始过滤这个elems集合了! for ( ; (elem = elems[i]) != null; i++ ) { if ( byElement && elem ) { j = 0; //把所有的过滤器拿出来 //这里的每个匹配器都是上边提到的终极匹配器,既然是终极匹配器,那为什么会有多个呢? //因为:div, p实际上是需要两个终极匹配器的(逗号分隔开表示有两个选择器了),:) while ( (matcher = elementMatchers[j++]) ) { //过滤这个集合的元素elem,如果符合匹配器的规则,那么就添加到结果集里边去 if ( matcher( elem, context, xml ) ) { results.push( elem ); break; } } if ( outermost ) { dirruns = dirrunsUnique; cachedruns = ++matcherCachedRuns; } } // Track unmatched elements for set filters //这里的bySet是为了跟踪setMatchers匹配器,这里以后再剖析 if ( bySet ) { // They will have gone through all possible matchers if ( (elem = !matcher && elem) ) { matchedCount--; } // Lengthen the array for every element, matched or not if ( seed ) { unmatched.push( elem ); } } } // Apply set filters to unmatched elements // 这里的bySet是为了跟踪setMatchers匹配器,这里以后再剖析 matchedCount += i; if ( bySet && i !== matchedCount ) { j = 0; while ( (matcher = setMatchers[j++]) ) { matcher( unmatched, setMatched, context, xml ); } if ( seed ) { // Reintegrate element matches to eliminate the need for sorting if ( matchedCount > 0 ) { while ( i-- ) { if ( !(unmatched[i] || setMatched[i]) ) { setMatched[i] = pop.call( results ); } } } // Discard index placeholder values to get only actual matches setMatched = condense( setMatched ); } // Add matches to results push.apply( results, setMatched ); // Seedless set matches succeeding multiple successful matchers stipulate sorting if ( outermost && !seed && setMatched.length > 0 && ( matchedCount + setMatchers.length ) > 1 ) { Sizzle.uniqueSort( results ); } } // Override manipulation of globals by nested matchers if ( outermost ) { dirruns = dirrunsUnique; outermostContext = contextBackup; } return unmatched; }; |
在下篇文章里,剖析一下Sizzle里边匹配器的构造方式,也就是上边源码提到的matcherFromTokens。这里涉及到一些比较绕的地方,可能剖析过程有误,欢迎交流指正。
本文链接:jQuery源码剖析(八)——Sizzle选择器引擎之解析原理
转载声明:本博客文章若无特别说明,皆为原创,转载请注明来源:拉风的博客,谢谢!
看远
这个写没有前面的好懂
raphealguo
SIZZLE部分确实复杂点,不过也不知道怎么样好写一点,你可以就那部分说明一下哪里不懂,我看看如何修改文章
看远
从compile后就读不懂了。先tokenize(selector)将字符分开标记,然后试着找出初级结果集,然后是怎么处理的?
只是简单的代码注释和说明,搞不明白,最好能像前面一样,说下接下来是打算干什么。
还有后期的文章什么时候更,期待中。
raphealguo
这里很难写太仔细,我回头想想怎么改比较好,后续更新的时间也不知道,看看我自己精力了
感觉还不错
感觉还不错啊!
匿名
谢谢分享!