搜索
首页web前端js教程jQuery中选择器引擎Sizzle的解析

jQuery中选择器引擎Sizzle的解析

Jul 14, 2018 am 09:30 AM
javascriptjquery选择器

这篇文章主要介绍了关于jQuery中选择器引擎Sizzle的解析,有着一定的参考价值,现在分享给大家,有需要的朋友可以参考一下

读Sizzle的源码,分析的Sizzle版本号是2.3.3

浏览器原生支持的元素查询方法:

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

        // 如果碰到 祖先/兄弟 关系(&#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 );
}

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的查询过程。

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函数的实现原理

以上是jQuery中选择器引擎Sizzle的解析的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
JavaScript,C和浏览器之间的关系JavaScript,C和浏览器之间的关系May 01, 2025 am 12:06 AM

引言我知道你可能会觉得奇怪,JavaScript、C 和浏览器之间到底有什么关系?它们之间看似毫无关联,但实际上,它们在现代网络开发中扮演着非常重要的角色。今天我们就来深入探讨一下这三者之间的紧密联系。通过这篇文章,你将了解到JavaScript如何在浏览器中运行,C 在浏览器引擎中的作用,以及它们如何共同推动网页的渲染和交互。JavaScript与浏览器的关系我们都知道,JavaScript是前端开发的核心语言,它直接在浏览器中运行,让网页变得生动有趣。你是否曾经想过,为什么JavaScr

node.js流带打字稿node.js流带打字稿Apr 30, 2025 am 08:22 AM

Node.js擅长于高效I/O,这在很大程度上要归功于流。 流媒体汇总处理数据,避免内存过载 - 大型文件,网络任务和实时应用程序的理想。将流与打字稿的类型安全结合起来创建POWE

Python vs. JavaScript:性能和效率注意事项Python vs. JavaScript:性能和效率注意事项Apr 30, 2025 am 12:08 AM

Python和JavaScript在性能和效率方面的差异主要体现在:1)Python作为解释型语言,运行速度较慢,但开发效率高,适合快速原型开发;2)JavaScript在浏览器中受限于单线程,但在Node.js中可利用多线程和异步I/O提升性能,两者在实际项目中各有优势。

JavaScript的起源:探索其实施语言JavaScript的起源:探索其实施语言Apr 29, 2025 am 12:51 AM

JavaScript起源于1995年,由布兰登·艾克创造,实现语言为C语言。1.C语言为JavaScript提供了高性能和系统级编程能力。2.JavaScript的内存管理和性能优化依赖于C语言。3.C语言的跨平台特性帮助JavaScript在不同操作系统上高效运行。

幕后:什么语言能力JavaScript?幕后:什么语言能力JavaScript?Apr 28, 2025 am 12:01 AM

JavaScript在浏览器和Node.js环境中运行,依赖JavaScript引擎解析和执行代码。1)解析阶段生成抽象语法树(AST);2)编译阶段将AST转换为字节码或机器码;3)执行阶段执行编译后的代码。

Python和JavaScript的未来:趋势和预测Python和JavaScript的未来:趋势和预测Apr 27, 2025 am 12:21 AM

Python和JavaScript的未来趋势包括:1.Python将巩固在科学计算和AI领域的地位,2.JavaScript将推动Web技术发展,3.跨平台开发将成为热门,4.性能优化将是重点。两者都将继续在各自领域扩展应用场景,并在性能上有更多突破。

Python vs. JavaScript:开发环境和工具Python vs. JavaScript:开发环境和工具Apr 26, 2025 am 12:09 AM

Python和JavaScript在开发环境上的选择都很重要。1)Python的开发环境包括PyCharm、JupyterNotebook和Anaconda,适合数据科学和快速原型开发。2)JavaScript的开发环境包括Node.js、VSCode和Webpack,适用于前端和后端开发。根据项目需求选择合适的工具可以提高开发效率和项目成功率。

JavaScript是用C编写的吗?检查证据JavaScript是用C编写的吗?检查证据Apr 25, 2025 am 12:15 AM

是的,JavaScript的引擎核心是用C语言编写的。1)C语言提供了高效性能和底层控制,适合JavaScript引擎的开发。2)以V8引擎为例,其核心用C 编写,结合了C的效率和面向对象特性。3)JavaScript引擎的工作原理包括解析、编译和执行,C语言在这些过程中发挥关键作用。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

SublimeText3 英文版

SublimeText3 英文版

推荐:为Win版本,支持代码提示!

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

SecLists

SecLists

SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。

适用于 Eclipse 的 SAP NetWeaver 服务器适配器

适用于 Eclipse 的 SAP NetWeaver 服务器适配器

将Eclipse与SAP NetWeaver应用服务器集成。