Heim  >  Artikel  >  Web-Frontend  >  Analyse von Sizzle, der Selektor-Engine in jQuery

Analyse von Sizzle, der Selektor-Engine in jQuery

不言
不言Original
2018-07-14 09:30:511952Durchsuche

Dieser Artikel stellt hauptsächlich die Analyse der Selektor-Engine Sizzle in jQuery vor. Jetzt kann ich ihn mit Ihnen teilen.

Lesen Sie den Quellcode von Sizzle und analysieren Sie es. Die Sizzle-Versionsnummer ist 2.3.3.

Elementabfragemethoden, die von Browsern nativ unterstützt werden:

方法名 方法描述 兼容性描述
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+

In Sizzle wird aus Leistungsgründen der Verwendung der nativen Methoden von JS für die Abfrage Vorrang eingeräumt . Von den oben aufgeführten Methoden werden mit Ausnahme der nicht verwendeten Methode querySelector alle anderen in Sizzle verwendet.

In Fällen, in denen die Ergebnisse nicht direkt mit nativen Methoden abgerufen werden können, muss Sizzle eine lexikalische Analyse durchführen, den komplexen CSS-Selektor zerlegen und dann Element für Element abfragen und filtern, um die Elemente zu erhalten, die schließlich die Abfragebedingungen erfüllen .

Es gibt mehrere Punkte, um die Geschwindigkeit dieser Low-Level-Abfrage zu verbessern:

  • Von rechts nach links: Der traditionelle Selektor ist Von links Rechts zum Beispiel besteht der Abfrageprozess für den Selektor #box .cls a darin, zuerst das Element von id=box zu finden und dann nach dem Element, das cls enthält, in der Klasse im Nachkommenknoten dieses Elements zu suchen Suchen Sie unter diesem Element nach allen a Elementen. Nachdem die Suche abgeschlossen ist, kehren Sie zur vorherigen Ebene zurück und suchen weiter nach dem nächsten .cls-Element usw., bis der Vorgang abgeschlossen ist. Ein Problem bei diesem Ansatz besteht darin, dass es viele Elemente gibt, die die Bedingungen nicht erfüllen und bei der Suche durchlaufen werden. Für die Reihenfolge von rechts nach links werden zunächst alle a-Elemente gefunden und dann die #box .cls-Elemente herausgefiltert, die diese Bedingung erfüllen, basierend auf den verbleibenden Selektoren a. Auf diese Weise wird der Abfrageumfang begrenzt und die Geschwindigkeit ist im Vergleich natürlich höher. Es muss jedoch klar sein, dass nicht alle Selektoren für diese Rechts-nach-Links-Abfrage geeignet sind. Nicht alle Rechts-nach-Links-Abfragen sind schneller als Links-nach-rechts-Abfragen, aber sie decken die überwiegende Mehrheit der Abfragesituationen ab.

  • Begrenzter Startwertsatz: Wenn es nur einen Satz von Selektoren gibt, d. h. keine durch Kommas getrennte Abfragebedingung, wird zuerst der letzte Knoten durchsucht , und dann Filtern vom letzten Knotensatz;

  • Abfragebereich begrenzen: Wenn der übergeordnete Knoten nur eine ID ist und keine anderen Einschränkungen enthält, wird die Abfrage Der Geltungsbereich wird auf den übergeordneten Knoten beschränkt. #box a

  • Cache-spezifische Daten: Hauptsächlich in drei Kategorien unterteilt: tokenCache, compileCache, classCache; Klasse)

Abfrage mit Standort-Pseudoklasse

  1. Einfacher Prozess

    Einfacher Vorgang Befolgen Sie bei Abfragen den
  2. Fluss von rechts nach links
  3. .

    Schauen Sie sich den einfachen Prozess an
Sizzle-Flussdiagramm (vereinfachte Version)

Was der einfache Prozess ignoriert, ist hauptsächlich die Verarbeitungslogik im Zusammenhang mit Positionspseudoklassen, z als: n-tes Kind

Lexikalische Analyse

Die lexikalische Analyse analysiert den String-Selektor in eine Reihe von TOKEN.

Lassen Sie uns zunächst klarstellen, dass TOKEN als kleinstes Atom betrachtet werden kann und nicht geteilt werden kann. In CSS-Selektoren wird TOKEN im Allgemeinen in Form von TAG, ID, CLASS, ATTR usw. ausgedrückt. Ein komplexer CSS-Selektor generiert nach der lexikalischen Analyse eine Reihe von TOKEN und führt dann die endgültige Abfrage und Filterung basierend auf diesen Token durch.

Das Folgende ist ein Beispiel, um den Prozess der lexikalischen Analyse zu veranschaulichen. Zum Parsen der Zeichenfolge

:

/**
 * 下面是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 );
    }
}

Nach dem obigen Parsing-Vorgang wird

in ein Array in der folgenden Form analysiert:

Sizzle: tokens

Kompilieren Funktion

Der Prozess des Kompilierens der Funktion ist sehr einfach. Gehen Sie laut #box .cls a zunächst zum Matcher-Cache, um den entsprechenden Matcher zu finden.

Wenn die gleiche #box .cls a-Abfrage schon einmal durchgeführt wurde und der Cache noch vorhanden ist (da die Anzahl der Sizzle-Ersetzungen begrenzt ist, wird bei Überschreiten der Anzahlbegrenzung der früheste Cache gelöscht), der aktuell Der zwischengespeicherte Matcher wird direkt zurückgegeben.

Wenn es nicht im Cache gefunden wird, wird der ultimative Matcher über die Methoden

und

generiert und der ultimative Matcher zwischengespeichert.

selectorMatcher basierend auf Token generieren (matcherFromTokens)

In diesem Schritt werden Matcher (Matcher) basierend auf den durch lexikalische Analyse erzeugten Token generiert. selectorIn Sizzle ist die entsprechende Methode

.

matcherFromTokens()matcherFromGroupMatchers()Lassen Sie sich impfen. Diese Methode ist sehr mühsam zu lesen.

Im Sizzle-Quellcode (

-Datei)
gibt es nur 60 Zeilen, aber es sind viele Factory-Methoden enthalten (es bezieht sich nur auf die Methode, deren Rückgabewert Function ist Typ). matcherFromTokensVereinfachen wir den Prozess dieser Methode (Entfernen der Verarbeitung von Pseudoklassenselektoren)

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++ ) {

        // 如果碰到 祖先/兄弟 关系(&#39;>', ' ', '+', '~'),则需要合并之前的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 );
}

Frage: Warum, wenn wir auf die Vorfahren-/Geschwisterbeziehung ('>', ' ', '+', '~'), müssen Sie die vorherigen Matcher zusammenführen?

sizzle.jsAntwort:第 1705 ~ 1765 行Der Zweck besteht nicht unbedingt darin, zusammenzuführen, sondern den zugehörigen Knoten des aktuellen Knotens zu finden (der die Beziehung zwischen Vorfahren und Geschwistern erfüllt ['>', ' ', '+ ', '~ ']) und verwenden Sie dann den vorherigen Matcher, um zu überprüfen, ob der zugehörige Knoten den Matcher erfüllt. Im Schritt „Verifizierung“ ist es nicht erforderlich, die vorherigen Matcher zusammenzuführen, aber die zusammengeführte Struktur wird klarer. Zum Beispiel:

我们需要买汽车,现在有两个汽车品牌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就是为了验证当前的节点的关联节点。

生成终极匹配器(matcherFromGroupMatchers)

主要是返回一个匿名函数,在这个函数中,利用matchersFromToken方法生成的匹配器,去验证种子集合seed,筛选出符合条件的集合。
先确定种子集合,然后在拿这些种子跟匹配器逐个匹配。在匹配的过程中,从右向左逐个token匹配,只要有一个环节不满条件,则跳出当前匹配流程,继续进行下一个种子节点的匹配过程。

通过这样的一个过程,从而筛选出满足条件的DOM节点,返回给select方法。

查询过程demo

用一个典型的查询,来说明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中文网!

相关推荐:

Debounce函数和Throttle函数的实现原理

Das obige ist der detaillierte Inhalt vonAnalyse von Sizzle, der Selektor-Engine in jQuery. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn