jQuery中的选择器引擎Sizzle

读Sizzle的源码,理会的Sizzle版本号是2.3.3

Sizzle的Github主页

浏览器原生支撑的元素查询要领:

要领名要领形貌兼容性形貌
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+

在Sizzle中,出于机能斟酌,优先斟酌运用JS的原生要领举行查询。上面列出的要领中,除了querySelector要领没有被用到,别的都在Sizzle中有运用。

关于不能够运用原生要领直接猎取效果的case,Sizzle就须要举行词法理会,理会这个庞杂的CSS挑选器,然后再逐项查询过滤,猎取终究相符查询前提的元素。

有以下几个点是为了进步这类低级别查询的速率:

  • 从右至左: 传统的挑选器是从左至右,比方关于挑选器#box .cls a,它的查询历程是先找到id=box的元素,然后在这个元素子女节点里查找class中包括cls元素;找到后,再查找这个元素下的一切a元素。查找完成后再回到上一层,继承查找下一个.cls元素,云云来去,直至完成。如许的做法有一个题目,就是有许多不相符前提元素,在查找也会被遍历到。而关于从右向左的递次,它是先找到一切a的元素,然后在依据剩下的挑选器#box .cls,挑选出相符这个前提的a元素。如许一来,等于是限定了查询局限,相对而言速率固然会更快。然则须要明白的一点是,并非一切的挑选器都合适这类从右至左的体式格局查询。也并非一切的从右至左查询都比从左至右快,只是它覆蓋了绝大多数的查询状况。
  • 限定种子鸠合: 假如只需一组挑选器,也就是不存在逗号分开查询前提的状况;则先查找最末级的节点,在最末级的节点鸠合中挑选;
  • 限定查询局限: 假如父级节点只是一个ID且不包括别的限定前提,则将查询局限缩小到父级节点;#box a
  • 缓存特定数据 : 重要分三类,tokenCache, compileCache, classCache

我们对Sizzle的查询分为两类:

  1. 浅易流程(没有位置伪类)
  2. 带位置伪类的查询

浅易流程

浅易流程在举行查询时,遵照 从右至左的流程。

梳理一下浅易流程

Sizzle流程图(浅易版)

浅易流程疏忽的东西重如果和位置伪类相干的处置惩罚逻辑,比方:nth-child之类的

词法理会

词法理会,将字符串的挑选器,理会成一系列的TOKEN。

起首明白一下TOKEN的观点,TOKEN能够看作最小的原子,不可再拆分。在CSS挑选器中,TOKEN的表现情势平常是TAG、ID、CLASS、ATTR等。一个庞杂的CSS挑选器,经由词法理会后,会天生一系列的TOKEN,然后依据这些Token举行终究的查询和挑选。

下面举个例子申明一下词法理会的历程。关于字符串#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会被理会成以下情势的数组:
Sizzle: tokens

编译函数

编译函数的流程很简单,起首依据selector去婚配器的缓存中查找对应的婚配器。

假如之前举行过雷同selector的查询而且缓存还在(由于Sizzle换粗数目有限,假如凌驾数目限定,最早的缓存会被删掉),则直接返回当前缓存的婚配器。

假如缓存中找不到,则经由过程matcherFromTokens()matcherFromGroupMatchers() 要领天生最终婚配器,并将最终婚配器缓存。

依据tokens天生婚配器(matcherFromTokens)

这一步是依据词法理会产出的tokens,天生matchers(婚配器)。
在Sizzle中,对应的要领是matcherFromTokens

打个预防针,这个要领读起来,很劳神呐。

在Sizzle源码(sizzle.js文件)中第 1705 ~ 1765 行,只需60行,却揉进了很多工场要领(就仅仅指那种return值是Function范例的要领)。
我们简化一下这个要领的流程(去掉了伪类挑选器的处置惩罚)

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 );
}

Question:为何假如遇到 先人/兄弟 关联(’>’, ‘ ‘, ‘+’, ‘~’),则须要兼并之前的matchers?

Answer:目标并不一定要兼并,而是为了找到当前节点关联节点(满足 先人/兄弟 关联[‘>’, ‘ ‘, ‘+’, ‘~’]),然后应用之前的婚配器考证这个关联节点是不是满足婚配器。而在“考证”这个环节并不一定非要兼并之前的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就是为了考证当前的节点的关联节点。

天生最终婚配器(matcherFromGroupMatchers)

重如果返回一个匿名函数,在这个函数中,应用matchersFromToken要领天生的婚配器,去考证种子鸠合seed,挑选出相符前提的鸠合。
先肯定种子鸠合,然后在拿这些种子跟婚配器逐一婚配。在婚配的历程当中,从右向左逐一token婚配,只需有一个环节不满前提,则跳出当前婚配流程,继承举行下一个种子节点的婚配历程。

经由过程如许的一个历程,从而挑选出满足前提的DOM节点,返回给select要领。

查询历程demo

用一个典范的查询,来申明Sizzle的查询历程。

div.cls input[type="text"] 为例:

理会出的tokens:

[
    [
        { "value": "div", "type": "TAG", "matches": ["div"] }, 
        { "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(‘div’)
  • matcherByClass(‘.cls’)

遇见父子关联' ',将前面的天生的两个matcher兼并天生一个新的

  • matcher:

    • matcherByTag(‘div’),
    • matcherByClass(‘.cls’)

这个matcher 是经由过程addCombinator()要领天生的匿名函数,这个matcher会先依据 父子关联parentNode,获得当前种子的parentNode, 然后再考证是不是满足前面的两个婚配器。

遇见第四条 属性挑选器,天生

  • matcherByAttr(‘[type=”text”]’)

至此,依据tokens已天生一切的matchers。

最终婚配器

  • matcher:

    • matcherByTag(‘div’)
    • 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 );
            }
        }
    });
}

【参考资料】

IE浏览器的兼容性查询

JQuery – Sizzle挑选器引擎道理理会

jQuery源码理会(七)——Sizzle挑选器引擎之词法理会

jQuery 2.0.3 源码理会Sizzle引擎 – 词法理会

Optimize Selectors(挑选器运用的最好体式格局)

    原文作者:胡斐
    原文地址: https://segmentfault.com/a/1190000015619484
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞