美文网首页jQuery源码笔记.jpg
jQuery源码二周目#11 Sizzle 预编译

jQuery源码二周目#11 Sizzle 预编译

作者: 柠檬果然酸 | 来源:发表于2020-10-25 08:38 被阅读0次

    经过词法解析之后做什么?
    遍历解析好的Token序列去生成一个个匹配器,在Expr.filter中已经提前写好了全部的TAGIDCLASSATTRCHILDPSEUDONAME7种匹配器。然后将DOM元素挨个与生成的匹配器匹配,如果所有匹配器都返回true,那该DOM元素就是我们需要的DOM元素。

    预编译

    其实Sizzle源码的处理和上面说的有些不一样,上面说的是将DOM元素挨个匹配,其实在源码中不是这样的。源码中是遍历Token序列生成对应的元匹配器,然后将多个元匹配器融合成一个超级匹配器,这样就能用一个方法完成匹配。

    // 预编译
    Sizzle.compile = function(selector, match) {
        var
            i,
            elementMatchers = [],
            cached;
    
        i = match.length;
        while(i--) {
            cached = matcherFromTokens(match[i]);
            elementMatchers.push(cached);
        }
    
        return matcherFromGroupMatchers(elementMatchers);
    }
    

    这是我简化过的Sizzle.compile()方法,源码中很多的干扰项都被我除掉了。这里面有两个重要的方法:matcherFromTokensmatcherFromGroupMatchers

    matcherFromTokens

    生成用于匹配单个选择器群组的函数

    function matcherFromTokens(tokens) {
        var
            i,
            token,
            matcher,
            matchers = [];
    
        for (i = 0; i < tokens.length; i++) {
            token = tokens[i];
    
            if (matcher = Expr.relative[token.type]) { // 生成层级匹配器
                matchers = [addCombinator(elementMatcher(matchers), matcher)];
            } else {
                // 问题:为什么这里不写成matcher = Expr.filter[token.type](token.matches);
                // 因为ATTR类型的匹配器需要传入三个参数
                matcher = Expr.filter[token.type].apply(null, token.matches);
                matchers.push(matcher);
            }
        }
    
        return elementMatcher(matchers);
    }
    

    div > p + .aaron[type="checkbox"], #id:first-child这个选择器中有两组tokens,分别将div > p + .aaron[type="checkbox"]#id:first-child传入matcherFromTokens中进行编译,这也是为什么Sizzle.compile()方法中要使用while循环去遍历match的原因。

    编译无非是将token取出来生成元匹配器,然后放入到matchers中,最后通过elementMatcher()方法将所有匹配器整合成一个方法并输出。生成匹配器什么的都很简单啦,因为在Expr.filter中已经写好了,直接拿过来用即可。真正难的是处理层级关系>, +, 空格, ~,Sizzle源码是通过addCombinator()方法去处理的。

    还有一个细节就是tokens的遍历方式是从左到右,为什么是这样自己脑内模拟程序执行流程或许就能明白。这东西太过抽象无法用语言描述,只能自行体会。

    elementMatcher

    这个方法很简单,就是将匹配器集合遍历挨个执行

    function elementMatcher( matchers ) {
        return matchers.length > 1 ?
    
            // 如果是多个匹配器
            function( elem, context, xml ) {
                var i = matchers.length;
                while ( i-- ) {
                    if ( !matchers[ i ]( elem, context, xml ) ) {
                        return false;
                    }
                }
                return true;
            } :
    
            // 如果是单个匹配器,返回自己即可
            matchers[ 0 ];
    }
    

    注意这里遍历顺序又是从右往左

    addCombinator

    这个方法也很难去讲解,只有自行调试才能理解

    // 层级关系处理器
    function addCombinator(matcher, combinator) {
        var dir = combinator.dir;
    
        return combinator.first ?
    
            // 紧密型
            function( elem, context, xml ) {
                while ( ( elem = elem[ dir ] ) ) {
    
                    // 加这么一个判断是因为有的时候拿到的是text类型的元素
                    if ( elem.nodeType === 1 ) {
                        return matcher( elem, context, xml );
                    }
                }
                return false;
            } :
    
            // 非紧密型一直递归查询,查到最后为null会跳出循环并且返回false
            // 如果递归过程中某一层返回true
            function( elem, context, xml ) {
                while ( ( elem = elem[ dir ] ) ) {
                    if ( elem.nodeType === 1 ) {
                        if (matcher(elem, context, xml)) {
                            return true;
                        }
                    }
                }
                return false;
            }
    }
    

    matcherFromGroupMatchers

    这个方法很容易理解,选择器div > p + .aaron[type="checkbox"], #id:first-child会生成两组matcherFromTokens匹配器,将DOM元素分别匹配两个匹配器,只要有一个匹配器返回为true,就将DOM元素保存,最后一起返回。

    // 超级匹配器(多组)
    // 像div > p + .aaron[type="checkbox"], #id:first-child这种选择器有两组matcherFromTokens匹配器
    // 该方法的作用就是将两组matcherFromTokens匹配器合并成一个匹配器
    function matcherFromGroupMatchers(elementMatchers) {
        return function (seed, results) {
            var i,
                j,
                elems,      // 种子合集
                elem,       // 种子合集中的单个DOM元素
                matcher;    // matcherFromTokens匹配器
    
            // 如果没有种子元素则全文检索
            elems = seed || Expr.find["TAG"]("*");
    
            // 遍历所有元素
            for (i = 0; i < elems.length; i++) {
                j = 0;
                elem = elems[i];
    
                // 遍历所有matcherFromTokens匹配器
                // 把elems中的元素挨个放入elementMatchers匹配器中
                // 若返回为true,将该元素放入results集合中
                while (matcher = elementMatchers[j++]) {
                    if (matcher(elem)) {
                        results.push(elem);
                    }
                }
            }
        }
    }
    

    最后

    Sizzle.select = function(selector) {
        var
            seed,           // 种子集合:类型为ID、TAG、CLASS中任意一种的DOM元素集合
            match,          // 词法解析后的数据
            results = []    // 目标元素
            ;
    
        // 词法解析
        match = Sizzle.tokenize(selector);
    
        // 预编译
        var superMatcher = Sizzle.compile(selector, match);
        superMatcher(seed, results);
    
        return results;
    }
    

    总结

    整个Sizzle源码学下来,我只学会了如何使用闭包。可能有人觉得只学会了一个东西也太废了,先听我把话说完。我觉得整个Sizzle源码最有价值的部分就是预编译(Sizzle.compile)部分,elementMatcher是预编译里面最重要的方法,说白了它不过就是个while循环,但它和普通while循环不一样的地方是它用闭包的技巧将被循环的对象保存下来,保证后续调用的时候能够取出来。

    至于其他的如何词法解析、如何筛选seed合集、匹配器怎么写的都显得不那么重要了。闭包这个概念我也不是第一次见了,以前在书上看到过很多次关于它的讲解,但是我从来都没学会怎么去使用。通过这次的源码学习,我能够亲眼看见别人是如何去玩转闭包这个东西的。果然学习光靠看书是没用的,实践才是检验真理的唯一标准。

    在此献上源码
    传送门

    相关文章

      网友评论

        本文标题:jQuery源码二周目#11 Sizzle 预编译

        本文链接:https://www.haomeiwen.com/subject/ydevmktx.html