当前位置: 首页 > 工具软件 > Sizzle > 使用案例 >

jQuery源码分析系列(二)Sizzle选择器引擎-上

彭烨熠
2023-12-01

前言

我们继续从init()方法中的find()方法往下看,

jQuery.find = Sizzle;
...

find: function (selector) {
	/** ... */

    ret = this.pushStack([]);	// 还是调用的递归栈方法

    for (i = 0; i < len; i++) {
        jQuery.find(selector, self[i], ret);	// 寻找selector,也就是进入Sizzle构造函数
    }

    return len > 1 ? jQuery.uniqueSort(ret) : ret;
},

选择器入口:Sizzle() 构造函数

执行流程:

  1. 首先再次判断选择器是否无效,无效则直接return;
  2. 根据有无种子(最后结果是其子集)传入,无种子则顺序直接,有种子则直接进入Select()
  3. 然后利用setDocument验证浏览器对各个属性的支持情况,此函数同时还提供各种匹配函数;
  4. 接着判断选择器类型:
    1. 根据选择器的复杂程度,先区分出ID、TAGS、CLASS三种选择器。其中ID选择器根据上下文的不同分两种情况,其他的直接调用浏览器函数得到结果即可;
    2. 如果选择器很复杂(不是上述三类选择器),则在方法内部调用tokenize()实现复杂的结构解析,再利用querySelectorAll()函数进行查找,如果此方法不兼容选择器则进入Selct()方法。
  5. 相对于前面的方法,Select()承载了重要的作用,其既能够实现复杂结构选择器的定位,也能够实现在种子中匹配相应子集,相应的是其难以理解,因为增加了更多的代码和匹配器
// Sizzle构造函数
function Sizzle(selector, context, results, seed) {
    var m, i, elem, nid, match, groups, newSelector,
        newContext = context && context.ownerDocument,

        // 如果没有传入上下文,则节点类型默认为9
        nodeType = context ? context.nodeType : 9;

    results = results || [];

    // 如果是无效的选择器或者文本,则直接返回
    if (typeof selector !== "string" || !selector ||
        // 1--元素, 9--文档,11-轻量级文档对象
        nodeType !== 1 && nodeType !== 9 && nodeType !== 11) { 

        return results;
    }

    // 尝试在HTML文档中使用快捷方式查找操作(而不是过滤器)
    if (!seed) {
        // 查看各个浏览器对各种操作的支持,并提供expr.find()校验方式
        /*********************  		***********************/
        setDocument(context);			
        /*********************			***********************/
        
        context = context || document;

        if (documentIsHTML) {	// 定义在setDocument文件中,表示其不是XML节点

            // 如果选择器能够快速被检测出,直接用get**by**
            if (nodeType !== 11 && (match = rquickExpr.exec(selector))) {

                // ID selector
                if ((m = match[1])) {

                    // Document context // 也就是 nodeType === 9
                    if (nodeType === 9) {
                        if ((elem = context.getElementById(m))) {

                            // 在IE,Opera,webkit中,getElementById可以按名称
                            // 而不是ID匹配元素,因此需要用属性id的值重新比较
                            if (elem.id === m) { 
                                results.push(elem);
                                return results;
                            }
                        } else {
                            return results;
                        }

                        // Element context	// 也就是 nodeType === 1
                    } else {
                        if (newContext && (elem = newContext.getElementById(m)) &&
                            contains(context, elem) &&
                            elem.id === m) {

                            results.push(elem);
                            return results;
                        }
                    }

                    // Type selector		// TAGS 选择器
                } else if (match[2]) {
                    push.apply(results, context.getElementsByTagName(selector));
                    return results;

                    // Class selector		// class 类选择器
                } else if ((m = match[3]) && support.getElementsByClassName &&
                    context.getElementsByClassName) {

                    push.apply(results, context.getElementsByClassName(m));
                    return results;
                }
            }

            // Take advantage of querySelectorAll			// 利用CSS选择器查询
            if (support.qsa &&
                !nonnativeSelectorCache[selector + " "] &&
                (!rbuggyQSA || !rbuggyQSA.test(selector)) &&

                // Support: IE 8 only
                // Exclude object elements, 排除 对象元素
                (nodeType !== 1 || context.nodeName.toLowerCase() !== "object")) {

                newSelector = selector;
                newContext = context;

                // 上面的话的意思是:querySelectorAll()在计算子结合时会考虑根节点以外的元素,
                // 因此给列表中每个选择器添加一个前缀ID选择器,便于QSA识别
                if (nodeType === 1 &&
                    // 正则表达式验证
                    (rdescend.test(selector) || rcombinators.test(selector))) { 

                    // 展开同级选择器的上下文
                    newContext = rsibling.test(selector) && 		testContext(context.parentNode) || context;

                    // 如果浏览器支持 :scope并且我们不改变上下文,则我们可以用 :scope替代ID
                    if (newContext !== context || !support.scope) {

                        // Capture the context ID, setting it first if necessary
                        if ((nid = context.getAttribute("id"))) { // 如果存在,则替换
                            // 字符串解码
                            nid = nid.replace(rcssescape, fcssescape);
                        } else { // 不能存在直接添加属性id
                            context.setAttribute("id", (nid = expando));
                        }
                    }

                    // 词义分析	
                    /**********************************************/
                    groups = tokenize(selector);
                    /**********************************************/
                    i = groups.lengthlength;
                    while (i--) {
                        groups[i] = (nid ? "#" + nid : ":scope") + " " +
                            toSelector(groups[i]);
                    }
                    newSelector = groups.join(",");
                }

                try {
                    push.apply(results,
             // 此方法不能检测复杂的String,如"#aa .bb",只能通过select()
                        newContext.querySelectorAll(newSelector) 
                    );
                    return results;
                } catch (qsaError) {
                    nonnativeSelectorCache(selector, true);
                } finally {
                    if (nid === expando) {
                        context.removeAttribute("id");
                    }
                }
            }
        }
    }

    // 只有结构复杂的选择器才会执行到这步,多个父元素、伪类、限定符等
    return select(selector.replace(rtrim, "$1"), context, results, seed);
}

准备工作:setDocument()函数

因为不同的浏览器支持不同的方法,也不一定支持所有属性。为了能够顺利获得各种属性以供选择器定位,我们首先要获得各种浏览器对对象属性的支持情况,然后据此指定不同的拦截器和过滤器。这也是为了兼容不得不做的牺牲。

同时此函数内部定义了判定包含和排序两个函数,由于此函数只用于选择器定位,因此写在此处。

// setDocument()函数
setDocument = Sizzle.setDocument = function (node) {
    var hasCompare, subWindow,
        doc = node ? node.ownerDocument || node : preferredDoc;
	
    // Return early if doc is invalid or already selected
    if (doc == document || doc.nodeType !== 9 || !doc.documentElement) {
        return document;
    }

    // 更新全局变量
    document = doc;
    docElem = document.documentElement;
    documentIsHTML = !isXML(document);

 
    // eslint-disable-next-line eqeqeq
    if (preferredDoc != document &&
        (subWindow = document.defaultView) && subWindow.top !== subWindow) {

        // Support: IE 11, Edge
        if (subWindow.addEventListener) {
            subWindow.addEventListener("unload", unloadHandler, false);

            // Support: IE 9 - 10 only
        } else if (subWindow.attachEvent) {
            subWindow.attachEvent("onunload", unloadHandler);
        }
    }
	
    /* Attributes Judge,将属性支持情况保存在support中,这些属性将在后面的选择器判定中用到
    ---------------------------------------------------------------------- */
    // 这里的assert方法也是依赖函数注入的,旨在判断浏览器是否支持某属性
    // 传入的el是一个 document.createElement("fieldset");
    support.scope = assert(function (el) {		
        docElem.appendChild(el).appendChild(document.createElement("div"));
        return typeof el.querySelectorAll !== "undefined" &&
            !el.querySelectorAll(":scope fieldset div").length;
    });
    
	/**  ..........................
	*   ..........................
	*/

    support.getById = assert(function (el) {
        docElem.appendChild(el).id = expando;
        return !document.getElementsByName || !document.getElementsByName(expando).length;
    });

    /* 拦截器、过滤器等组件的注册
    ---------------------------------------------------------------------- */
    if (support.getById) {
        Expr.filter["ID"] = function (id) {
            var attrId = id.replace(runescape, funescape);	// 字符串解码
            return function (elem) {
                return elem.getAttribute("id") === attrId; // 找到ID
            };
        };
        Expr.find["ID"] = function (id, context) {
            if (typeof context.getElementById !== "undefined" && documentIsHTML) {
                var elem = context.getElementById(id);
                return elem ? [elem] : []; // 存在返回数组,否则返回空数组
            }
        };
    } else {	// 如果浏览器不支持获取ID,则利用getAttributeNode()
        Expr.filter["ID"] = function (id) {
            var attrId = id.replace(runescape, funescape);
            return function (elem) {
                 // 使用 getAttributeNode() 方法从当前元素中通过名称获取属性节点
                var node = typeof elem.getAttributeNode !== "undefined" &&
                    elem.getAttributeNode("id"); 。
                return node && node.value === attrId;
            };
        };

        // Support: IE 6 - 7 only
        //getElement作为捷径寻找并不是可信赖的
        Expr.find["ID"] = function (id, context) {
            // 如果context存在ID属性
            if (typeof context.getElementById !== "undefined" && documentIsHTML) { 	
                var node, i, elems,
                    elem = context.getElementById(id);

                if (elem) {
                    // Verify the id attribute
                    node = elem.getAttributeNode("id"); // 一般到这里就return了,
                    if (node && node.value === id) {
                        return [elem];
                    }

                   	// fall back on ...  
                    elems = context.getElementsByName(id); // 通过属性name定位元素
                    i = 0;
                    while ((elem = elems[i++])) {
                        node = elem.getAttributeNode("id");
                        if (node && node.value === id) {
                            return [elem];
                        }
                    }
                }

                return [];
            }
        };
    }

    // Tag
    //	p.s.:细心的你会发现后面的Expr对象的filter已经定义了大部分,而find为空,原来是在这里定义
    Expr.find["TAG"] = support.getElementsByTagName ? 
        function (tag, context) {
            if (typeof context.getElementsByTagName !== "undefined") {
                return context.getElementsByTagName(tag);

                // DocumentFragment nodes don't have gEBTN
            } else if (support.qsa) {
                return context.querySelectorAll(tag);
            }
        } :

        function (tag, context) {
            var elem,
                tmp = [],
                i = 0,

               
                // 巧合的是,一个DocumentFragment节点上也出现了一个gEBTN
                results = context.getElementsByTagName(tag); 

            // Filter out possible comments
            if (tag === "*") {	 				// 如果是匹配 "*",可能有一些不符合条件的出来
                while ((elem = results[i++])) {
                    if (elem.nodeType === 1) {
                        tmp.push(elem); 		// 将results中所有元素类型为1的返回
                    }
                }

                return tmp;
            }
            return results;
        };

    // Class
    Expr.find["CLASS"] = support.getElementsByClassName && function (className, context) {
        if (typeof context.getElementsByClassName !== "undefined" && documentIsHTML) {
            return context.getElementsByClassName(className);
        }
    };

    /* QSA/matchesSelector
    ---------------------------------------------------------------------- */

    // QSA和匹配选择器支持。同上面相同,这里还是做一些准备工作

    // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
    rbuggyMatches = [];

    rbuggyQSA = []; // 存储匹配正则表达式的数组

    if ((support.qsa = rnative.test(document.querySelectorAll))) {

        // 构建QSA正则表达式,Regex strategy adopted from Diego Perini
        assert(function (el) {

            var input;

            docElem.appendChild(el).innerHTML = "<a id='" + expando + "'></a>" +
                "<select id='" + expando + "-\r\\' msallowcapture=''>" +
                "<option selected=''></option></select>";

            if (el.querySelectorAll("[msallowcapture^='']").length) {
                rbuggyQSA.push("[*^$]=" + whitespace + "*(?:''|\"\")");
            }

            // Support: IE8
            // Boolean attributes and "value" are not treated correctly
            if (!el.querySelectorAll("[selected]").length) {
                rbuggyQSA.push("\\[" + whitespace + "*(?:value|" + booleans + ")");
            }

            if (!el.querySelectorAll("[id~=" + expando + "-]").length) {
                rbuggyQSA.push("~=");
            }

            input = document.createElement("input");
            input.setAttribute("name", "");
            el.appendChild(input);
            if (!el.querySelectorAll("[name='']").length) {
                rbuggyQSA.push("\\[" + whitespace + "*name" + whitespace + "*=" +
                    whitespace + "*(?:''|\"\")");
            }

            if (!el.querySelectorAll(":checked").length) {
                rbuggyQSA.push(":checked");
            }

            if (!el.querySelectorAll("a#" + expando + "+*").length) {
                rbuggyQSA.push(".#.+[+~]");
            }

            el.querySelectorAll("\\\f");
            rbuggyQSA.push("[\\r\\n\\f]");
        });

        assert(function (el) {
            el.innerHTML = "<a href='' disabled='disabled'></a>" +
                "<select disabled='disabled'><option/></select>";

            var input = document.createElement("input");
            input.setAttribute("type", "hidden");
            el.appendChild(input).setAttribute("name", "D");

            // Support: IE8
            // Enforce case-sensitivity of name attribute
            if (el.querySelectorAll("[name=d]").length) {
                rbuggyQSA.push("name" + whitespace + "*[*^$|!~]?=");
            }

            if (el.querySelectorAll(":enabled").length !== 2) {
                rbuggyQSA.push(":enabled", ":disabled");
            }

            docElem.appendChild(el).disabled = true;
            if (el.querySelectorAll(":disabled").length !== 2) {
                rbuggyQSA.push(":enabled", ":disabled");
            }

            el.querySelectorAll("*,:x");
            rbuggyQSA.push(",.*:");
        });
    }

    if ((support.matchesSelector = rnative.test((matches = docElem.matches ||
        docElem.webkitMatchesSelector ||
        docElem.mozMatchesSelector ||
        docElem.oMatchesSelector ||
        docElem.msMatchesSelector)))) {

        assert(function (el) {

            // 检查是否可以在断开连接的节点上执行检测器
            support.disconnectedMatch = matches.call(el, "*"); // 获得el匹配 "*" 的元素

            // This should fail with an exception
            // Gecko does not error, returns false instead
            matches.call(el, "[s!='']:x");
            rbuggyMatches.push("!=", pseudos);
        });
    }

    rbuggyQSA = rbuggyQSA.length && new RegExp(rbuggyQSA.join("|"));
    rbuggyMatches = rbuggyMatches.length && new RegExp(rbuggyMatches.join("|"));


    /* Contains  	// 包含函数判断,同下面的排序函数相同,单纯的工具函数,原理应该掌握
    ---------------------------------------------------------------------- */
    // compareDocumentPosition,判断一个段落相比较另一个段落的位置:
    hasCompare = rnative.test(docElem.compareDocumentPosition);

    // Element contains another
    // Purposefully self-exclusive
    // As in, an element does not contain itself
    contains = hasCompare || rnative.test(docElem.contains) ?
        function (a, b) {
            var adown = a.nodeType === 9 ? a.documentElement : a,
                bup = b && b.parentNode;
            return a === bup || !!(bup && bup.nodeType === 1 && (
                adown.contains ?
                    adown.contains(bup) :
                    a.compareDocumentPosition && a.compareDocumentPosition(bup) & 16
            ));
        } :
        function (a, b) {
            if (b) {
                while ((b = b.parentNode)) { // 往上回溯,直到b === a或者 b是根节点
                    if (b === a) {
                        return true;
                    }
                }
            }
            return false;
        };

    /* Sorting		// 节点排序函数
    ---------------------------------------------------------------------- */
    // Document order sorting			
    sortOrder = hasCompare ?	// 如果节点是可以比较的
        function (a, b) {
            
            if (a === b) {
                hasDuplicate = true;
                return 0;
            }

            var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
            if (compare) {
                return compare;
            }
        
			// 如果两者具有相同的根元素,则直接比较位置
            compare = (a.ownerDocument || a) == (b.ownerDocument || b) ? 
                a.compareDocumentPosition(b) :

                // Otherwise we know they are disconnected
                1;

            // Disconnected nodes
            if (compare & 1 || // 如果不具有相同根元素,则
                (!support.sortDetached && b.compareDocumentPosition(a) === compare)) {
                // 如果a是根节点,则返回-1
                if (a == document || a.ownerDocument == preferredDoc && 
                    contains(preferredDoc, a)) {
                    return -1;
                }

                if (b == document || b.ownerDocument == preferredDoc &&
                    contains(preferredDoc, b)) {
                    return 1;
                }

                // Maintain original order
                return sortInput ?
                    // indexof(a,b) 返回b在(类数组)a中的位置
                    (indexOf(sortInput, a) - indexOf(sortInput, b)) : 
                    0;
            }

            return compare & 4 ? -1 : 1;
        } :
        function (a, b) {

            // Exit early if the nodes are identical
            if (a === b) {
                hasDuplicate = true;
                return 0;
            }

            var cur,
                i = 0,
                aup = a.parentNode,
                bup = b.parentNode,
                ap = [a],
                bp = [b];

            // Parentless nodes are either documents or disconnected
            // 没有父亲节点,要么是文档,要么是断开的
            if (!aup || !bup) {

                return a == document ? -1 :
                    b == document ? 1 :
                        /* eslint-enable eqeqeq */
                        aup ? -1 :
                            bup ? 1 :
                                sortInput ?
                                    (indexOf(sortInput, a) - indexOf(sortInput, b)) :
                                    0;

             	// 如果两者具有相同的父亲
            } else if (aup === bup) {
                return siblingCheck(a, b);
            }

            // 对比他们所有的祖先
            cur = a;
            while ((cur = cur.parentNode)) {
                ap.unshift(cur); // arr.unshift() 向数组开头添加一个或多个元素,并返回新数组长度
            }
            cur = b;
            while ((cur = cur.parentNode)) {
                bp.unshift(cur);
            }

            // Walk down the tree looking for a discrepancy
            while (ap[i] === bp[i]) {
                i++; // 定位到祖先不同的点
            }

词法解析函数:tokenize()

词法分析,从本质上来说使用一些列规定好的拦截器、过滤器来截取我们需要的词,请带着此理念去阅读下面代码

// tokenize两个作用:1.解析选择器;	2.将解析结果存入缓存
tokenize = Sizzle.tokenize = function (selector, parseOnly) {
    var matched, match, tokens, type,
        soFar, groups, preFilters,
        cached = tokenCache[selector + " "];

    // 如果tokenCache中已经有selector了,则直接拿出来就好了
    if (cached) {
        return parseOnly ? 0 : cached.slice(0);
    }

    soFar = selector;
    groups = [];
    // 这里的预处理器为了对匹配到的Token适当做一些调整
    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 = soFar.slice(match[0].length) || soFar;
            }
            groups.push((tokens = []));
        }

        matched = false;

        // Combinators		包含
        if ((match = rcombinators.exec(soFar))) {
            matched = match.shift(); // 去除第一个元素,并返回此元素的值
            tokens.push({
                value: matched,
                // 将后代组合子投射到空间
                type: match[0].replace(rtrim, " ")
            });
            soFar = soFar.slice(matched.length);
        }

        // Filters		过滤
        // 对soFar逐一匹配ID、TAG、CLASS、CHILD、ATTR、PSEUDO类型的过滤器方法
		
        for (type in Expr.filter) { 
            if (// 根据过滤器类型选择正则表达式检验选择器
                (match = matchExpr[type].exec(soFar)) && 
                // 如果预过滤器不存在此类型或预过滤器处理之后返回了有效值
                (!preFilters[type] ||(match = preFilters[type](match)))) { 
                matched = match.shift(); // 删除第一个元素
                tokens.push({
                    value: matched,
                    type: type,
                    matches: match
                });
                soFar = soFar.slice(matched.length);  // 从matched处开启截取,直到最后
            }
        }

        if (!matched) {
            break;
        }
    }

    // 如果只是解析的话,返回解析后的长度,否则抛出错误或返回解析结果
    return parseOnly ?
        soFar.length :
        soFar ?
            Sizzle.error(selector) :

            // Cache the tokens
            tokenCache(selector, groups).slice(0);	// 从0截取到最后
};

经过上面的分析,可以得到一个结论,那就是除了前面的jQuery对象创建期间的设置的几道拦截方法外,在Sizzle引擎中也对简单结构选择器进行了拦截,并且其使用大量代码来兼容不同的浏览器。

知识点:

  1. 拦截器的注册与使用:
  2. 节点元素的包含与排序:
  3. 选择器字符串词义分析:

结语

如果你输入的只是简单的单体选择器,那么上面讲到的解析过程足以实现。但事实往往并非如此,jQuery最其强大的功能之一就是可以根据复杂多变的选择器以及上下文环境,甚至规定备选种子来选取特定的元素,这就是下一篇要讲到的select()函数

 类似资料: