Cet article présente principalement l'analyse du moteur de sélection Sizzle dans jQuery. Il a une certaine valeur de référence. Maintenant, je le partage avec tout le monde. Les amis dans le besoin peuvent s'y référer
Lisez le code source de Sizzle et. analysez-le. Le numéro de version de Sizzle est 2.3.3
.
Méthodes de requête d'éléments supportées nativement par les navigateurs :
方法名 | 方法描述 | 兼容性描述 |
---|---|---|
getElementById | 根据元素ID查询元素 | IE6+, Firefox 2+, Chrome 4+, Safari 3.1+ |
getElementsByTagName | 根据元素名称查询元素 | IE6+, Firefox 2+, Chrome 4+, Safari 3.1+ |
getElementsByClassName | 根据元素的class查询元素 | IE9+, Firefox 3+, Chrome 4+, Safari 3.1+ |
getElementsByName | 根据元素name属性查询元素 | IE10+(IE10以下不支持或不完善), FireFox23+, Chrome 29+, Safari 6+ |
querySelector | 根据选择器查询元素 | IE9+(IE8部分支持), Firefox 3.5+, Chrome 4+, Safari 3.1+ |
querySelectorAll | 根据选择器查询元素 | IE9+(IE8部分支持), Firefox 3.5+, Chrome 4+, Safari 3.1+ |
Dans Sizzle, pour des raisons de performances, la priorité est donnée à l'utilisation des méthodes natives de JS pour les requêtes . Parmi les méthodes listées ci-dessus, à l'exception de la méthode querySelector
qui n'est pas utilisée, toutes les autres sont utilisées dans Sizzle.
Dans les cas où les résultats ne peuvent pas être obtenus directement à l'aide de méthodes natives, Sizzle doit effectuer une analyse lexicale, décomposer le sélecteur CSS complexe, puis interroger et filtrer élément par élément pour obtenir les éléments qui répondent finalement aux conditions de requête. .
Il y a plusieurs points pour améliorer la rapidité de cette requête de bas niveau :
De droite à gauche : Le sélecteur traditionnel est De gauche à droite, par exemple, pour le sélecteur #box .cls a
, son processus de requête consiste à rechercher d'abord l'élément de id=box
, puis à rechercher l'élément contenant cls
dans la classe dans le nœud descendant de cet élément après avoir trouvé ; dessus, recherchez sous cet élément Tous les éléments a
. Une fois la recherche terminée, revenez au niveau précédent et continuez à rechercher l'élément .cls
suivant, et ainsi de suite jusqu'à la fin. Un problème avec cette approche est que de nombreux éléments ne remplissent pas les conditions et seront parcourus lors de la recherche. Pour l'ordre de droite à gauche, il recherche d'abord tous les éléments a
, puis filtre les éléments #box .cls
qui remplissent cette condition en fonction des sélecteurs restants a
. De cette façon, la portée de la requête est limitée et la vitesse sera bien entendu relativement plus rapide. Mais il faut être clair : tous les sélecteurs ne conviennent pas à cette requête de droite à gauche. Toutes les requêtes de droite à gauche ne sont pas plus rapides que celles de gauche à droite, mais elles couvrent la grande majorité des situations de requête.
Ensemble de graines limité : s'il n'y a qu'un seul ensemble de sélecteurs, c'est-à-dire qu'il n'y a pas de condition de requête séparée par des virgules, alors le dernier nœud est recherché en premier ; , puis Filtrer à partir du dernier nœud défini ;
Limiter la portée de la requête : Si le nœud parent est simplement un identifiant et ne contient pas d'autres restrictions, la requête la portée sera restreinte Au nœud parent #box a
;
Données spécifiques au cache : Principalement divisées en trois catégories, tokenCache, compileCache, classCache; Class)
Requête avec pseudo-classe d'emplacement
Processus simple
.
Ce que le processus simple ignore, c'est principalement la logique de traitement liée aux pseudo-classes de position, telles comme : nième enfant Analyse lexicale
L'analyse lexicale analyse le sélecteur de chaîne en une série de TOKEN.
Tout d’abord, clarifions le concept de TOKEN peut être considéré comme le plus petit atome et ne peut pas être divisé. Dans les sélecteurs CSS, la forme d'expression de TOKEN est généralement TAG, ID, CLASS, ATTR, etc. Un sélecteur CSS complexe générera une série de TOKEN après analyse lexicale, puis effectuera une requête finale et un filtrage basés sur ces jetons.
Ce qui suit est un exemple pour illustrer le processus d’analyse lexicale. Pour l'analyse de la chaîne
:Après le processus d'analyse ci-dessus,
sera analysé dans un tableau sous la forme suivante :Sizzle : tokens
Compiler Le processus de compilation de la fonction #box .cls a
/** * 下面是Sizzle中词法解析方法 tokennize 的核心代码 1670 ~ 1681 行 * soFar = '#box .cls a' * Expr.filter 是Sizzle进行元素过滤的方法集合 * Object.getOwnPropertyNames(Expr.filter) // ["TAG", "CLASS", "ATTR", "CHILD", "PSEUDO", "ID"] */ for ( type in Expr.filter ) { // 拿当前的选择字符串soFar 取匹配filter的类型,如果能匹配到,则将当前的匹配对象取出,并当做一个Token存储起来 // matchExpr中存储一些列正则,这些正则用于验证当前选择字符串是否满足某一token语法 if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || (match = preFilters[ type ]( match ))) ) { matched = match.shift(); tokens.push({ value: matched, type: type, matches: match }); // 截取掉匹配到选择字符串,继续匹配剩余的字符串(继续匹配是通过这段代码外围的while(soFar)循环实现的) // matchExpr中存储的正则都是元字符“^”开头,验证字符串是否以‘xxx’开头;这也就是说, 词法分析的过程是从字符串开始位置,从左至右,一下一下地剥离出token soFar = soFar.slice( matched.length ); } }
#box .cls a
Si la même
requête a été effectuée auparavant et que le cache est toujours là (car le nombre de remplacements Sizzle est limité, si la limite de nombre est dépassée, le cache le plus ancien sera supprimé), le cache actuellement le matcher mis en cache sera renvoyé directement.
et le matcher ultime est mis en cache. selector
selector
Cette étape consiste à générer des matchers (matchers) basés sur les jetons produits par l'analyse lexicale.
. matcherFromTokens()
matcherFromGroupMatchers()
Dans le code source Sizzle (
fichier) matcherFromTokens
, il n'y a que 60 lignes, mais beaucoup de méthodes d'usine sont incluses (cela fait uniquement référence à la méthode dont la valeur de retour est Function taper).
Question :
Pourquoi si on rencontre la relation ancêtre/frère ('> ' , ' ', '+', '~'), avez-vous besoin de fusionner les correspondants précédents ? sizzle.js
第 1705 ~ 1765 行
Réponse :
function matcherFromTokens( tokens ) { var checkContext, matcher, j, len = tokens.length, leadingRelative = Expr.relative[ tokens[0].type ], implicitRelative = leadingRelative || Expr.relative[" "], i = leadingRelative ? 1 : 0, // The foundational matcher ensures that elements are reachable from top-level context(s) matchContext = addCombinator( function( elem ) { return elem === checkContext; }, implicitRelative, true ), matchAnyContext = addCombinator( function( elem ) { return indexOf( checkContext, elem ) > -1; }, implicitRelative, true ), matchers = [ function( elem, context, xml ) { var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( (checkContext = context).nodeType ? matchContext( elem, context, xml ) : matchAnyContext( elem, context, xml ) ); // Avoid hanging onto element (issue #299) checkContext = null; return ret; } ]; // 上面的都是变量声明 // 这个for循环就是根据tokens 生成matchers 的过程 for ( ; i < len; i++ ) { // 如果碰到 祖先/兄弟 关系('>', ' ', '+', '~'),则需要合并之前的matchers; if ( (matcher = Expr.relative[ tokens[i].type ]) ) { matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; } else { matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); matchers.push( matcher ); } } // 将所有的matchers 拼合到一起 返回一个匹配器, // 所有的matcher返回值都是布尔值,只要有一个条件不满足,则当前元素不符合,排除掉 return elementMatcher( matchers ); }
我们需要买汽车,现在有两个汽车品牌A、B。A下面有四种车型:a1,a2,a3,a4;B下面有两种车型:b1,b2。那么我们可以的买到所有车就是
[a1,a2,a3,a4,b1,b2]。但是我们也可以这么写{A:[a1,a2,a3,a4],B:[b1,b2]}。这两种写法都可以表示我们可以买到车型。只是第二种相对前者,更清晰列出了车型所属品牌关系。
同理,在合并后,我们就知道这个合并后的matcher就是为了验证当前的节点的关联节点。
主要是返回一个匿名函数,在这个函数中,利用matchersFromToken方法生成的匹配器,去验证种子集合seed,筛选出符合条件的集合。
先确定种子集合,然后在拿这些种子跟匹配器逐个匹配。在匹配的过程中,从右向左逐个token匹配,只要有一个环节不满条件,则跳出当前匹配流程,继续进行下一个种子节点的匹配过程。
通过这样的一个过程,从而筛选出满足条件的DOM节点,返回给select
方法。
用一个典型的查询,来说明Sizzle的查询过程。
以 p.cls input[type="text"]
为例:
解析出的tokens:
[ [ { "value": "p", "type": "TAG", "matches": ["p"] }, { "value": ".cls", "type": "CLASS", "matches": ["cls"] }, { "value": " ", "type": " " }, { "value": "input", "type": "TAG", "matches": ["input"] }, { "value": "[type=\"text\"]", "type": "ATTR", "matches": ["type", "=", "text"]} ] ]
首先这个选择器 会筛选出所有的<input>
作为种子集合seed,然后在这个集合中寻找符合条件的节点。
在寻找种子节点的过程中,删掉了token中的第四条{ "value": "input", "type": "TAG", "matches": ["input"] }
。
那么会根据剩下的tokens生成匹配器
matcherByTag('p')
matcherByClass('.cls')
碰见父子关系' '
,将前面的生成的两个matcher合并生成一个新的
matcher:
matcherByTag('p'),
matcherByClass('.cls')
这个matcher 是通过addCombinator()
方法生成的匿名函数,这个matcher会先根据 父子关系parentNode
,取得当前种子的parentNode, 然后再验证是否满足前面的两个匹配器。
碰见第四条 属性选择器,生成
matcherByAttr('[type="text"]')
至此,根据tokens已经生成所有的matchers。
终极匹配器
matcher:
matcherByTag('p')
matcherByClass('.cls')
matcherByAttr('[type="text"]')
在matcherFromTokens()
方法中的最后一行,还有一步操作,将所有的matchers通过elementMatcher()
合并成一个matcher。elementMatcher
这个方法就是将所有的匹配方法,通过while循环都执行一遍,如果碰到不满足条件的,就直接挑出while循环。
有一点需要说明的就是: elementMatcher
方法中的while循环是倒序执行的,即从matchers最后一个matcher开始执行匹配规则。对应上面的这个例子就是,最开始执行的匹配器是matcherByAttr('[type="text"]')。 这样一来,就过滤出了所有不满足type="text"
的<input>
的元素。然后执行下一个匹配条件,
Question: Sizzle中使用了大量闭包函数,有什么作用?出于什么考虑的?
Answer:闭包函数的作用,是为了根据selector
动态生成匹配器,并将这个匹配器缓存(cached)。因为使用闭包,匹配器得以保存在内存中,这为缓存机制提供了支持。
这么做的主要目的是提高查询性能,通过常驻内存的匹配器避免再次消耗大量资源进行词法分析和匹配器生成。以空间换时间,提高查询速度。
Question: matcherFromTokens中, 对每个tokens生成匹配器列表时,为什么会有一个初始化的方法?
Answer: 这个初始化的方法是用来验证元素是否属于当前context
。
Question: matcherFromGroupMatchers的作用?
Answer: 返回一个终极匹配器,并让编译函数缓存这个终极匹配器。 在这个终极匹配器中,会将获取到的种子元素集合与匹配器进行比对,筛选出符合条件的元素。
TODO: 编译机制也许是Sizzle为了做缓存以便提高性能而做出的选择??
是的,详细答案待补充~~~
TODO: outermostContext的作用
细节问题,还有待研究~~~
带位置伪类的查询是 由左至右。
用选择器.mark li.limark:first.limark2 a span
举例。
在根据tokens生成匹配器(matcherFromTokens)之前的过程,跟简易查询没有任何区别。
不同的地方就在matcherFromTokens()
方法中。位置伪类不同于简易查询的是,它会根据位置伪类将选择器分成三个部分。对应上例就是如下
.mark li.limark
: 位置伪类之前的选择器;
:first
: 位置伪类本身;
.limark2
: 跟位置伪类本身相关的选择器,
a span
:位置伪类之后的选择器;
位置伪类的查询思路,是先进行位置伪类之前的查询.mark li.limark
,这个查询过程当然也是利用之前讲过的简易流程(Sizzle(selector))。查询完成后,再根据位置伪类进行过滤,留下满足位置伪类的节点。如果存在第三个条件,则利用第三个条件,再进行一次过滤。然后再利用这些满足位置伪类节点作为context,进行位置伪类之后选择器 a span
的查询。
上例选择器中只存在一个位置伪类;如果存在多个,则从左至右,会形成一个一个的层级,逐个层级进行查询。
下面是对应的是matcherFromTokens()
方法中对位置伪类处理。
// 这个matcherFromTokens中这个for循环,之前讲过了,但是 有个地方我们跳过没讲 for ( ; i < len; i++ ) { if ( (matcher = Expr.relative[ tokens[i].type ]) ) { matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; } else { matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); // Return special upon seeing a positional matcher // 这个就是处理位置伪类的逻辑 if ( matcher[ expando ] ) { // Find the next relative operator (if any) for proper handling j = ++i; for ( ; j < len; j++ ) { // 寻找下一个关系节点位置,并用j记录下来 if ( Expr.relative[ tokens[j].type ] ) { break; } } return setMatcher(// setMatcher 是生成位置伪类查询的工厂方法 i > 1 && elementMatcher( matchers ), // 位置伪类之前的matcher i > 1 && toSelector( // If the preceding token was a descendant combinator, insert an implicit any-element `*` tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) ).replace( rtrim, "$1" ), // 位置伪类之前的selector matcher, // 位置伪类本身的matcher i < j && matcherFromTokens( tokens.slice( i, j ) ), // 位置伪类本身的filter j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), // 位置伪类之后的matcher j < len && toSelector( tokens ) // 位置伪类之后的selector ); } matchers.push( matcher ); } }
setMatcher()
方法的源码,在这里生成最终的matcher, return给compile()
方法。
//第1个参数,preFilter,前置过滤器,相当于伪类token之前`.mark li.limark`的过滤器matcher //第2个参数,selector,伪类之前的selector (`.mark li.limark`) //第3个参数,matcher, 当前位置伪类的过滤器matcher `:first` //第4个参数,postFilter,伪类之后的过滤器 `.limark2` //第5个参数,postFinder,后置搜索器,相当于在前边过滤出来的集合里边再搜索剩下的规则的一个搜索器 ` a span`的matcher //第6个参数,postSelector,后置搜索器对应的选择器字符串,相当于` a span` function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { //TODO: setMatcher 会把这俩货在搞一次setMatcher, 还不太懂 if ( postFilter && !postFilter[ expando ] ) { postFilter = setMatcher( postFilter ); } if ( postFinder && !postFinder[ expando ] ) { postFinder = setMatcher( postFinder, postSelector ); } return markFunction(function( seed, results, context, xml ) { var temp, i, elem, preMap = [], postMap = [], preexisting = results.length, // Get initial elements from seed or context elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), // Prefilter to get matcher input, preserving a map for seed-results synchronization matcherIn = preFilter && ( seed || !selector ) ? condense( elems, preMap, preFilter, context, xml ) : elems, matcherOut = matcher ? // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, postFinder || ( seed ? preFilter : preexisting || postFilter ) ? // ...intermediate processing is necessary [] : // ...otherwise use results directly results : matcherIn; // Find primary matches if ( matcher ) { // 这个就是 匹配位置伪类的 逻辑, 将符合位置伪类的节点剔出来 matcher( matcherIn, matcherOut, context, xml ); } // Apply postFilter if ( postFilter ) { temp = condense( matcherOut, postMap ); postFilter( temp, [], context, xml ); // Un-match failing elements by moving them back to matcherIn i = temp.length; while ( i-- ) { if ( (elem = temp[i]) ) { matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); } } } if ( seed ) { if ( postFinder || preFilter ) { if ( postFinder ) { // Get the final matcherOut by condensing this intermediate into postFinder contexts temp = []; i = matcherOut.length; while ( i-- ) { if ( (elem = matcherOut[i]) ) { // Restore matcherIn since elem is not yet a final match temp.push( (matcherIn[i] = elem) ); } } postFinder( null, (matcherOut = []), temp, xml ); } // Move matched elements from seed to results to keep them synchronized i = matcherOut.length; while ( i-- ) { if ( (elem = matcherOut[i]) && (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { seed[temp] = !(results[temp] = elem); } } } // Add elements to results, through postFinder if defined } else { matcherOut = condense( matcherOut === results ? matcherOut.splice( preexisting, matcherOut.length ) : matcherOut ); if ( postFinder ) { postFinder( null, results, matcherOut, xml ); } else { push.apply( results, matcherOut ); } } }); }
以上就是本文的全部内容,希望对大家的学习有所帮助,更多相关内容请关注PHP中文网!
相关推荐:
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!