前言
上篇文章《jQuery源码剖析(六)——类数组》最后聊到了jQuery所用到的选择器引擎Sizzle。
接下来的几篇文章会剖析一下关于Sizzle这个引擎。首先当然是选择器的概念以及Sizzle是如何分析这个选择器的。
CSS选择器
我们知道浏览器最终会将HTML文档(或者说页面)解析成一棵DOM树,如下代码将会翻译成以下的DOM树。
1 2 3 4 5 6 7 8 9 | <div> <p> <input type="text" /> </p> <div class="clr"> <input type="checkbox" name="readme" /> <p>Hello World</p> </div> </div> |
为了操作到当中那个checkbox,我们需要有一种表述方式,使得通过这个表达式让浏览器知道我们是想要操作哪个DOM节点。
这个表述方式就是CSS选择器,它是这样表示的:div > p + .clr input[type="checkbox"]
表达的意思是,div底下的p的兄弟节点,该节点的class为clr,并且其属性type为checkbox。
常见的选择器:
- #test表示id为test的DOM节点
- .clr表示class为clr的DOM节点
- input表示节点名为input的DOM节点
- div > p表示div底下的p的DOM节点
- div + p表示div的兄弟DOM节点p
具体的选择器如何表达,这里就不阐述了,详细可以自己网上搜一下或者见《CSS选择器》
浏览器提供了一些接口可以让javascript取到DOM树的某个节点。
- document.getElementById(“test”),获取id为test的DOM节点
- document.getElementsByTagName(“input”),获取节点名为input的DOM节点
- document.getElementsByName(“checkbox”),获取属性name为checkbox的DOM节点
高级的浏览器还提供document.getElementsByClassName,用来获取class为参数值的DOM节点。其实浏览器能够直接提供一个接口,我们直接把选择器表达式传进去,然后其返回一个符合规则的DOM节点列表。
其实在高级浏览器里边,这个接口是存在的,它就是:document.querySelectorAll。由于低级浏览器并未提供这些高级点的接口,所以才有了Sizzle这个CSS选择器引擎。Sizzle引擎提供的接口跟document.querySelectorAll是一样的,其输入是一串选择器字符串,输出则是一个符合这个选择器规则的DOM节点列表,因此第一步骤是要分析这个输入的选择器。
词法分析
先看一段简单的代码:alert(“Hello World”);
浏览器拿到这段代码(其实就是字符串)时,要如何开始解析呢?js解释器需要从左到右扫描字符:
a -> al -> ale -> aler -> alert -> alert( -> 通过左括号辨别出此时alert是一个函数名,于是给一个标志位FUNCTION,把左括号(标志位LEFT_BRACKET
紧接着认出后边是一个字符串Hello World,标志为STRING,最后是右括号RIGHT_BRACKET以及分号SEMI
我们把FUNCTION,LEFT_BRACKET,STRING,RIGHT_BRACKET,SEMI称为Token,把代码解析成Token的阶段在编译阶段里边称为词法分析。
代码经过词法分析后就得到了一个Token序列,紧接着拿Token序列去其他事情,在这里就不阐述了。
回到这篇文章的主题,CSS选择器其实也就是一段字符串,我们需要分析出这个字符串背后对应的规则,在这里Sizzle用了简单的词法分析。
在Sizzle里边,分析出来的Token类型(词素)有以下几种类型:TAG, ID, CLASS, ATTR, CHILD, PSEUDO, NAME, >, +, 空格, ~。如果你大概了解了CSS选择器,不难从这些名字上辨认出它们分别对应是什么选择器
Sizzle处理过程
如下图,对于Sizzle来说,输入就是选择器字符串,输出result是一个DOM节点列表。
本篇文章关注的是词法分析阶段。
Sizzle的词法分析
源码注释:
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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | //假设传入进来的选择器是:div > p + .clr[type="checkbox"], #id:first-child //这里可以分为两个规则:div > p + .clr[type="checkbox"] 以及 #id:first-child //返回的需要是一个Token序列 //Sizzle的Token格式如下 :{value:'匹配到的字符串', type:'对应的Token类型', matches:'正则匹配到的一个结构'} function tokenize( selector, parseOnly ) { var matched, match, tokens, type, soFar, groups, preFilters, cached = tokenCache[ selector + " " ]; //这里的soFar是表示目前还未分析的字符串剩余部分 //groups表示目前已经匹配到的规则组,在这个例子里边,groups的长度最后是2,存放的是每个规则对应的Token序列 //如果cache里边有,直接拿出来即可 if ( cached ) { return parseOnly ? 0 : cached.slice( 0 ); } //初始化 soFar = selector; groups = []; //这里的预处理器为了对匹配到的Token适当做一些调整 //自行查看源码,其实就是正则匹配到的内容的一个预处理 preFilters = Expr.preFilter; //当字符串还没解析完毕,循环开始 while ( soFar ) { // Comma and first run if ( !matched || (match = rcomma.exec( soFar )) ) { if ( match ) { //如果匹配到逗号 // Don't consume trailing commas as valid soFar = soFar.slice( match[0].length ) || soFar; } //往规则组里边压入一个Token序列,目前Token序列还是空的 groups.push( tokens = [] ); } matched = false; // Combinators //先处理这几个特殊的Token : >, +, 空格, ~ //因为他们比较简单,并且是单字符的 if ( (match = rcombinators.exec( soFar )) ) { //获取到匹配的字符 matched = match.shift(); //放入Token序列中 tokens.push( { value: matched, // Cast descendant combinators to space type: match[0].replace( rtrim, " " ) } ); //剩余还未分析的字符串需要减去这段已经分析过的 soFar = soFar.slice( matched.length ); } // Filters //这里开始分析这几种Token : TAG, ID, CLASS, ATTR, CHILD, PSEUDO, NAME //Expr.filter里边对应地 就有这些key for ( type in Expr.filter ) { /* 这里的正则如下,其实可以忽略此段。 自己写一下正则也不会特别难 matchExpr = { "ID": new RegExp( "^#(" + characterEncoding + ")" ), "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ), "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), "ATTR": new RegExp( "^" + attributes ), "PSEUDO": new RegExp( "^" + pseudos ), "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), // For use in libraries implementing .is() // We use this for POS matching in `select` "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) }, */ //如果通过正则匹配到了Token格式:match = matchExpr[ type ].exec( soFar ) //然后看看需不需要预处理:!preFilters[ type ] //如果需要 ,那么通过预处理器将匹配到的处理一下 : match = preFilters[ type ]( match ) if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || (match = preFilters[ type ]( match ))) ) { matched = match.shift(); //放入Token序列中 tokens.push( { value: matched, type: type, matches: match } ); //剩余还未分析的字符串需要减去这段已经分析过的 soFar = soFar.slice( matched.length ); } } //如果到了这里都还没matched到,那么说明这个选择器在这里有错误 //直接中断词法分析过程 //这就是Sizzle对词法分析的异常处理 if ( !matched ) { break; } } // Return the length of the invalid excess // if we're just parsing // Otherwise, throw an error or return tokens //如果只需要这个接口检查选择器的合法性,直接就返回soFar的剩余长度,倘若是大于零,说明选择器不合法 //其余情况,如果soFar长度大于零,抛出异常;否则把groups记录在cache里边并返回, return parseOnly ? soFar.length : soFar ? Sizzle.error( selector ) : // Cache the tokens tokenCache( selector, groups ).slice( 0 ); } //词法分析阶段需要的缓存器 tokenCache = createCache(), /** * Create key-value caches of limited size * @returns {Function(string, Object)} Returns the Object data after storing it on itself with * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) * deleting the oldest entry */ // function createCache() { var cache, keys = []; return (cache = function( key, value ) { // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) // 为了避免缓存太多,导致内存使用过大,所以这里可以设置cache长度。默认是 // cacheLength: 50, if ( keys.push( key += " " ) > Expr.cacheLength ) { // Only keep the most recent entries //超过缓存,那就把缓存的第一个删掉 delete cache[ keys.shift() ]; } return (cache[ key ] = value); }); } |
这一篇文章只是奠定一下选择器的概念,并剖析Sizzle是如何做词法分析的。在下一篇我会剖析一下Sizzle大概的分析步骤以及为什么要这么做。
本文链接:jQuery源码剖析(七)——Sizzle选择器引擎之词法分析
转载声明:本博客文章若无特别说明,皆为原创,转载请注明来源:拉风的博客,谢谢!
写的很好
groups.push( tokens = [] );
这句话不会让groups === [[]]吗。不明白为什么能够把数据分成两节。
不好意思我懂了,是不是开始的时候就是要[[]],然后后面再在里面加数据,然后再一次遇到“,”的时候在把再压入一个。jQuery写的好精妙!
jQuery中的选择器引擎Sizzle – 1
[…] jQuery源码剖析(七)——Sizzle选择器引擎之词法分析 […]
jQuery中的选择器引擎Sizzle - 算法网
[…] jQuery源码理会(七)——Sizzle挑选器引擎之词法理会 […]
jQuery中的选择器引擎Sizzle - 算法网
[…] jQuery源码剖析(七)——Sizzle选择器引擎之词法分析 […]