Sizzle源码分析(五) 如何根据css选择器,选中对应的html节点

495 阅读12分钟

简介

css 选择器是怎么来选中一个元素的呢?sizzle在css选中一个元素是从右到左开始选中一个元素的。原因是效率问题。请大家看下面的示例。如果我们要选择中 .header .nav .nav-item a 。如果我们从左往右的话,第一种查找:在第一个div中找到属性值为.header 的class,继续找到.nav 下的.nav-item,然后选中这个a标签的元素。 第二种查找:在标签 为main标签的子节点中一级一级的往下找,在class属性值为header的子节点中继续查找,class的属性值为.nav的元素,继续找到class属性值为.nav-item下发现没有a标签。那么就结束了,这样class属性值类似的非常多的话,我们要对逐个进行遍历。很浪费我们查询效率了。但是如果我们从a 这tag 从右往左开始查找,然后一直向上,加上验证是不是a 这个标签的父元素是不是就会减少了我们很多不必要的查找。从右向左这样查询效率是不是很显著的就提升上来?

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>
<body>
	<div class="header">
		<div class="nav">
			<div class="nav-item">
				<a href="#"></a>
			</div>
		</div>
	</div>
	<main>
		<div class="header">
			<div class="nav">
				<div class="nav-item">
				</div>
			</div>
		</div>
	</main>
</body>
</html>

sizzle是怎么来实现这些验证的?

同学们好,这里我思考了很久才来写下这边博客的,因为我发现有时候我写的源码解读过于空洞没有灵魂了,所以在这里写的时候加上我个人思考与我的认识。希望真正的写好一篇博客,而不是记流水账。我们sizzle来选中一个html元素也是通过上述的方法来选中一个元素的。先选中最右边的元素,然后一级一级的向上验证父元素,最后来确定到底有没有这个元素,希望大家耐心的看完这个两个例子。

例子1

我举个例子如在下面的html中选中这个 main nav .nav-item。 是否能选中,答案是肯定不能选中的。具体流程应该是这样,从右开始选中了.nav-item 。然后向上开始验证,父元素中有没有className是.nav的元素,如有就继续验证.nav上面有没有tag 为main的父元素。如有则返回选中的.nav-item。如果没有则返回null。明白了这些基本理念。

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>
<body>
	<div class="header">
		<div class="nav">
			<div class="nav-item">
				<a href=""></a>
			</div>
		</div>
	</div>
</body>
</html>

例子2

我们的选择器是这样的“.p-box p:nth-child(2n)”,sizzle选择器会把在词法解析的时候把解析成{type:"CLASS", value: 'p-box'}/{type:"TAG", value: 'p'}/{type:"CHILD", value: 'nth-child(2n)'}, 根据p 元素这个tag,然后拿到这个伪类选择器来过滤元素。最后会选中4个p元素返回出去。

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>
<body>
	<div class="p-box">
		<p>我是第1个</p>
		<p>我是第2个</p>
		<p>我是第3个</p>
		<p>我是第4个</p>
		<p>我是第5个</p>
		<p>我是第6个</p>
		<p>我是第7个</p>
		<p>我是第8个</p>
	</div>
</body>
</html>

大体的思路我们确定了下来,然后就开始看看是怎么选中元素,然后过滤元素,验证是不是我们要选中的元素的。

JavaScript是怎么来选中一个元素的

大家都知道选中可以元素可以利用标签的id属性、也可以用class属性、也可以用标签名来选中。以前的浏览器是不支持querySelectorAll 或者有一些缺陷,我们要实现这个querySelectorAll就要根据document.getElementById、document.getElementsByClassName、document.getElementsBytagName结合起来使用来实现这个API对吧。

刚开始我也说过了思路了如果我们使用的是从左到右与从右到左的两种选择元素的方式。假如我们输入了一串选择器“#header > .nav .nav-item” 。当着个字符串传入的时候,我们要把这一连串选择器拆分开来、然后进行分类,让他变成JavaScrip 能选中识别到的API。 比如我们要把#header拆分开来,在程序选择的时候知道他是id选择器器,> 是子代选择器,.nav是class选择器。然后对应相应的api来找到这个元素。我们最终要找的元素是哪个,肯定是.nav-item对吧。先找到.nav-item然后我们一级一级的向上做验证。ok到了这里相信大家都应该明白。下面讲的方法就想怎么来把这一连串选择器分类、加上标识、这里一般被称为词法解析。

tokenize 选择器 词法解析

我理解的这块词法解析是把我们单个选择器拆分成最小的粒子,加上各种标识,然后JavaScript明白这到底是什么选择器。

tokenize接受两个参数,它是从左到右来识别选择器的,把每个选择器组装成一个最小粒子,这个最小粒子包含它是什么类型选择器 比如#header { type: 'ID', value: 'header', matches: { 0: 'header', groups: undefined ....} }这种形式。这就是一个token,如果把上述的所有的选择器全部解析出来放入进去就是tokens,用一个数组来存储这些token,如果是加了并联选择器的话,就进行拆分。放入groups中,最后把groups返回出去。 这里我们把所有的选择器都给解析好了并且返回出去了,那么是不是要开始选中这些元素了。那就是接下来的一步了。select

/**
 * [tokenize]
 * @param  {[string]} selector  
 * @param  {[boolean]} parseOnly 
 * @return {[array]}          
 */
tokenize = Sizzle.tokenize = function(selector, parseOnly) {
    var matched, match, tokens, type,
            soFar, groups, preFilters,
            cached = tokenCache[selector + " "];

    if (cached) {
            return parseOnly ? 0 : cached.slice(0);
    }

    soFar = selector;
    groups = [];
    preFilters = Expr.preFilter;

    while (soFar) {
        // Comma and first run
        if (!matched || (match = rcomma.exec(soFar))) {
                if (match) {
                    // Don't consume trailing commas as valid
                    // 拿到“,”之后的选择器
                    soFar = soFar.slice(match[0].length) || soFar;
                }
                groups.push((tokens = []));
        }
        // 设置为false区分是否第一次,如果是第一次的话需要给groups添加一个空的,如果存在分组的话还需要继续分组
        matched = false;

            // Combinators
            // 查看是否有>+~ 之类的选择器或者 空格选择器,是否有组合选择器,
            // 如果有的话就拿掉选择器 这个选择器,重新计算选择器,推入tokens中,继续向下匹配其他简单class/tag/id/child/PSEUDO/attr类的组合器
            if ((match = rcombinators.exec(soFar))) {
                    matched = match.shift();
                    tokens.push({
                            value: matched,

                            // Cast descendant combinators to space
                            type: match[0].replace(rtrim, " ")
                    });
                    soFar = soFar.slice(matched.length);
            }

            // Filters
            for (type in Expr.filter) {
                    if ((match = matchExpr[type].exec(soFar)) && (!preFilters[type] ||
                                    (match = preFilters[type](match)))) {
                            // 截取选择器选中的第一个 比如.box 会去掉前面的.剩下了
                            // 0: "box"
                            // groups: undefined
                            // index: 0
                            // input: ".box > .header-wrapper .header .nav .nav-item"
                            // length: 1 这是一个类数组
                            matched = match.shift(); //.box
                            /**
                             * {
                             *   value: '.box',
                             *   type: 'class',
                             *   matches: {
                             *   	 0: "box",
                             *   	 groups: undefined,
                             *   	 index: 0,
                             *   	 input: ".box > .header-wrapper .header .nav .nav-item",
                             *   	 length: 1 这是一个类数组
                             *   }
                             * }
                             */
                            tokens.push({
                                    value: matched,
                                    type: type,
                                    matches: match
                            });
                            //让剩下的继续进行词法解析
                            soFar = soFar.slice(matched.length);
                    }
            }
            // 如果matched是false 就跳过本次循环,因为没有拿到词法解析的value值
            if (!matched) {
                    break;
            }
    }

select 选中的元素

我们上一步拿到了所有的选择器中的粒子化的一个有序的数组,那么接下来我给大家,已经在上述中说过了,我们选中一个元素是从右边开始的。选中最右边的然后开始,从右向左开始验证我们选中的元素是否是正确的。优势已经说过了。
match是我们拿到的这个有序的数组,比如说这样“#header > .nav .nav-item” [ { type: 'ID', value: 'header', matches: { 0: 'header', groups: undefined ....} }, ..., { { type: 'CLASS', value: 'nav-item', matches: { 0: 'nav-item', groups: undefined ....}}],最后一个是伪类选择器的话,我们就要选择中伪类选择器前面的元素然后根据伪类选择类型然后进行过滤了。好了如果match是长度是1的话,说明没有并联选择器。这样我们只用对一组选择器进行选中其中的元素了。
match[0]的长度大于2的时候,就看这一组的元素中第一个元素是不是ID选择器,如果是的话,我们直接对元素选中,然后改变上下文,比如说上下我们我们预制的是document,现在我们有一个元素id是header,我们只用选中id是header的元素,然后把它作为上下问,这样的话,我们查找范围就变小了。
然后我们拿到tokens的长度,从最后一个选中然后选中元素。到了这里基本就告一段落了,剩下的就是最终要的程序步骤了,我们要做的是验证这个选中的元素到底是不是正确的。这个留到下节。

/**
 * A low-level selection function that works with Sizzle's compiled
 *  selector functions
 * @param {String|Function} selector A selector or a pre-compiled
 *  selector function built with Sizzle.compile
 * @param {Element} context
 * @param {Array} [results]
 * @param {Array} [seed] A set of elements to match against
 */
select = Sizzle.select = function(selector, context, results, seed) {

    var i, tokens, token, type, find,
    //这里看是否有编译过的缓存
            compiled = typeof selector === "function" && selector,
              // tokenize的时候传入compiled的selector如果是属性,如果编译过了,则直接回返回编译过的结果,不用再次执行tokenize
            match = !seed && tokenize((selector = compiled.selector || selector));
    results = results || [];
    // Try to minimize operations if there is only one selector in the list and no seed
    // (the latter of which guarantees us context)
    // 如果match === 0是第一次,没有seed,这里做最小化搜索
    // 查看match到的词法解析了几组选择器,如果是一组着这里直接处理,如果不是一组的话就要进行编译。编译完了拿返回
    if (match.length === 1) {
            // Reduce context if the leading compound selector is an ID
            // 拿到第一组选择器Array,赋值给了tokens。
            // 如果第一个选择器是的0: {value: "#header", type: "ID", matches: Array(1)} ID选择器
            tokens = match[0] = match[0].slice(0);
            // 如果tokens的length大于0 的话,就看看tokens的第一个选择器是不是ID类型如果是的话context.nodeType 是9文档跟节点,documentIsHTML是html,然后Expr.relativet的tokens[1].type存在。就是存在是一个位置选择器。
            if (tokens.length > 2 && (token = tokens[0]).type === "ID" &&
                    // 而context的noType === 9 是html
                    // Expr.relative[tokens[1].type] 存在相对的位置
                    context.nodeType === 9 && documentIsHTML && Expr.relative[tokens[1].type]) {
                    // 这里拿到ID然后拿到这个拿到这个选择器#id,然后传入上下文,id这个元素作为上下文。
                    context = (Expr.find["ID"](token.matches[0]
                            .replace(runescape, funescape), context) || [])[0];
                    // 如果不存在这个ID,说明没有这些元素直接返回空数组
                    if (!context) {
                            return results;
                            // 预编译的匹配器仍然将验证祖先,因此请提高级别
                            // Precompiled matchers will still verify ancestry, so step up a level
                            //如果已经编译过了直接拿到到这个元素的父节点作为上下文
                    } else if (compiled) {
                            context = context.parentNode;
                    }
                    selector = selector.slice(tokens.shift().value.length);
            }
            // 测试是这个选择器是否需要上下文, 如果需要i等于0 ,不需要返回tokens的长度
            // Fetch a seed set for right-to-left matching
            //比如这个是id选择器#box > .header-wrapper .header .nav .nav-item,在上面中已经作为了上下文了,剩下的选择器 > .header-wrapper .header .nav .nav-item,到时候我们找剩下的选择器就好了。如果是>开始必须得知道是在 哪个上下文中的。这里是0不会再走while循环,直接对这些选择器进行一个编译 了。
            i = matchExpr["needsContext"].test(selector) ? 0 : tokens.length;
            // 这里查看是否
            // 这里对所有进行词法解析的词进行二次的 加工, 主要是查看是否有组合选择器
            // 如果是0的话就不走这里了,如果是从右到做匹配的话就走这里了。
            while (i--) {
                    // token = tokens[i]; 这里把css选择器从右到左开始 遍历
                    token = tokens[i];

                    // Abort if we hit a combinator
                    // 如果是碰到组合选择器就跳出这轮进行下轮循环
                    if (Expr.relative[(type = token.type)]) {
                            break;
                    }
                    // 通过type来确定选择器类型 type = token.type,拿到这个类型的方法
                    if ((find = Expr.find[type])) {
                            // 搜索,扩展领先统计组合器的上下文
                            // Search, expanding context for leading sibling combinators
                            // Expr.find["CLASS"] = support.getElementsByClassName && function(className, context) {
                            // 	if (typeof context.getElementsByClassName !== "undefined" && documentIsHTML) {
                            // 		return context.getElementsByClassName(className);
                            // 	}
                            // };
                            //  这里找到的元素放入seed中找到这个元素放入seed当中, find(tokens.maches[0], context)      
                            // 这里如果没有选择到元素,会在superMatch超级匹配器中找到所以的元素,一个一个的验证是否是要被选择中的元素
                            if ((seed = find(
                                            // 把class传进去
                                            token.matches[0].replace(runescape, funescape),
                                            // 测试看有没有兄弟元素选择器  测试有没有上下文
                                            rsibling.test(tokens[0].type) && testContext(context.parentNode) ||
                                            // 或者context的存在
                                            context
                                    ))) {
                                    // 如果查找到了这些元素就在tokens中删除这个词法
                                    // If seed is empty or no tokens remain, we can return early
                                    // 如果seed是空的或者没有token ,就直接return
                                    // {
                                    // value: 'class',
                                    // type: 'CLASS',
                                    // matched: [0: "header" roups: undefined index: 0 input: ".header .nav .nav-item" length: 1]
                                    // }
                                    tokens.splice(i, 1);
                                    /**
                                     * 解析的词法组
                                     * @param  {[type]} tokens [description]
                                     * @return {[type]}        [description]
                                            function toSelector(tokens) {
                                                    var i = 0,
                                                            len = tokens.length,
                                                            selector = "";
                                                    for (; i < len; i++) {
                                                            selector += tokens[i].value;
                                                    }
                                                    return selector;
                                            }
                                     */
                                    // 然后重新返回选择器
                                    selector = seed.length && toSelector(tokens);
                                    // 存在selector就是false
                                    // 如果选择器已经被解析完了那就直接返回元素
                                    if (!selector) {
                                            push.apply(results, seed);
                                            return results;
                                    }

                                    break;
                            }
                    }
            }
    }

    // Compile and execute a filtering function if one is not provided
    // Provide `match` to avoid retokenization if we modified the selector 
    // 如果没有提供, 编译和执行 过滤方法,提供match, 去避免重新进行词法解析, 如果我们改变 了选择器
    // 对剩下tokens中的选择器进行编译,也就是查看,选中的元素是否是符合规则的。
    // 如果编译过直接调用compile的方法。传入参数(seed, context, !documentIsHTML, results, rsibling.test(selector) && testContext(context.parentNode) || context)
    // 如果没有编译过就先进行编译然然后进行过滤
    (compiled || compile(selector, match))( // 种子元素已经有了
            seed,
            context, !documentIsHTML, // 上下文是不是html
            // reulst 0         快速测试有没有兄弟元素         测试上下文
            results, !context || rsibling.test(selector) && testContext(context.parentNode) || context
    );
    // 返回选中的元素
    return results;
};

Sizzle源码分析(一) 基本概念

Sizzle源码分析(二) 工具方法

Sizzle源码分析(三) 兼容处理

Sizzle源码分析(四) sizzle静态方法分析

Sizzle源码分析(五) 如何根据css选择器,选中对应的html节点