首页 > js教程 > 正文

jQuery中选择器引擎Sizzle的解析

原创 2018-07-14 09:30:51 0 597
这篇文章主要介绍了关于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++ ) {

        // 如果碰到 祖先/兄弟 关系('>', ' ', '+', '~'),则需要合并之前的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中文网其它相关文章!

  • 相关标签:
  • 本文原创发布php中文网 ,转载请注明出处,感谢您的尊重!
  • 网友评论

    文明上网理性发言,请遵守 新闻评论服务协议

    条评论

    独孤九贱(4)_PHP视频教程

    江湖传言:PHP是世界上最好的编程语言。真的是这样吗?这个梗究竟是从哪来的?学会本课程,你就会明白了。 PHP中文网出品的PHP入门系统教学视频,完全从初学者的角度出发,绝不玩虚的,一切以实用、有用...

    独孤九贱(5)_ThinkPHP5视频教程

    ThinkPHP是国内最流行的中文PHP开发框架,也是您Web项目的最佳选择。《php.cn独孤九贱(5)-ThinkPHP5视频教程》课程以ThinkPHP5最新版本为例,从最基本的框架常识开始,将...

    ThinkPHP5实战之[教学管理系统]

    本套教程,以一个真实的学校教学管理系统为案例,手把手教会您如何在一张白纸上,从零开始,一步一步的用ThinkPHP5框架快速开发出一个商业项目。

    独孤九贱(1)_HTML5视频教程

    《php.cn原创html5视频教程》课程特色:php中文网原创幽默段子系列课程,以恶搞,段子为主题风格的php视频教程!轻松的教学风格,简短的教学模式,让同学们在不知不觉中,学会了HTML知识。 ...

    PHP入门视频教程之一周学会PHP

    所有计算机语言的学习都要从基础开始,《PHP入门视频教程之一周学会PHP》不仅是PHP的基础部分更主要的是PHP语言的核心技术,是学习PHP必须掌握的内容,任何PHP项目的实现都离不开这部分的内容,通...

    ThinkPHP5快速开发企业站点[全程实录]更新中...

    本课以最新版ThinkPHP5.0.10为基础进行开发,全程实录一个完整企业点,从后台到前台,从控制器到路由的全套完整教程,不论是你是新人,还是有一定开发经验的程序员,都可以从中学到实用的知识~~

    Thinkphp3.2.3个人博客开发

    ThinkPHP是一个快速、开源的轻量级国产PHP开发框架,是业内最流行的PHP框架之一。本课程以博客系统为例,讲述如何使用TP实战开发,从中学习Thinkphp的实践应用。模版下载地址:http:/...

    PHP实战天龙八部之仿爱奇艺电影网站

    本课程是php实战开发课程,以爱奇艺电影网站为蓝本从零开发一个自己的网站。目的是让大家了解真实项目的架构及开发过程

    独孤九贱(8)_php从零开始开发属于自己的php框架

    本课以一个极简的PHP开发框架为案例,向您展示了一个PHP框架应该具有的基本功能,以及具体的实现方法,让您快速对PHP开发框架的底层实现有一个清楚的认识,为以后学习其实的开发框架打下坚实的基础。

    独孤九贱(3)_JavaScript视频教程

    javascript是运行在浏览器上的脚本语言,连续多年,被评为全球最受欢迎的编程语言。是前端开发必备三大法器中,最具杀伤力。如果前端开发是降龙十八掌,好么javascript就是第18掌:亢龙有悔。...

    直播实录:PHP魔鬼训练营[从零开始制作个人博客]

    本站9月直播课已经结束,本套教程是直播实录,没有报上名或者漏听学员福利来了,赶紧看看吧,说不定这里就有你的菜

    2018前端入门_HTML5

    轻松明快,简洁生动,让你快速走入HTML5的世界,体会语义化开发的魅力

    JavaScript极速入门_玉女心经系列

    JavaScript能够称得上是史上使用最广泛的编程语言,也是前端开发必须掌握的三技能之一:描述网页内容的HTML、描述网页样式的CSS以及描述网页行为的JavaScript。本章节将帮助大家迅速掌握...

    独孤九贱(7)_Bootstrap视频教程

    Bootstrap 是最受欢迎的 HTML、CSS 和 JS 框架,用于开发响应式布局、移动设备优先的 WEB 项目。为所有开发者、所有应用场景而设计,它让前端开发更快速、简单,所有开发者都能快速上手...

    PHP用户注册登录系统视频教程

    《php用户注册登录系统》主要介绍网站的登录注册功能,我们会从最简单的实现登录注册功能开始,增加验证码,cookie验证等,丰富网站的登录注册功能

    独孤九贱(2)_CSS视频教程

    《php.cn独孤九贱(2)-css视频教程》课程特色:php中文网原创幽默段子系列课程,以恶搞,段子为主题风格的php视频教程!轻松的教学风格,简短的教学模式,让同学们在不知不觉中,学会了CSS知识...

    PHP学生管理系统视频教程

    《PHP学生管理系统视频教程》主要给大家讲解了HTML,PHP,MySQL之间的相互协作,实现动态的网页显示和获取数据.

    独孤九贱(6)_jQuery视频教程

    jQuery是一个快速、简洁的JavaScript框架。设计的宗旨是“write Less,Do More”,即倡导写更少的代码,做更多的事情。它封装JavaScript常用的功能代码,提供一种简便的...

    弹指间学会HTML视频教程

    《弹指间学会HTML视频教程》从最基本的概念开始讲起,步步深入,带领大家学习HTML,了解各种常用标签的意义以及基本用法,学习HTML知识为以后的学习打下基础

    最新微信小程序开发视频教程

    《最新微信小程序开发视频教程》本节课程是由微趋道录制,讲述了如何申请一个微信小程序,以及开发中需要使用哪些工具,和需要注意哪些等。

    • 昧光

      全栈工程师

    • 认证0级讲师
    • 5318篇
      文章总数
    • 597
      文章总浏览数

    头条

    推荐视频教程

  • javascript初级视频教程
  • jquery 基础视频教程
  • javascript三级联动视频教程
  • 独孤九贱(3)_JavaScript视频教程
  • 独孤九贱(6)_jQuery视频教程
  • 最新更新