jQuery源码分析(十三): 选择器引擎Sizzle

前端基础 2016-11-28

起步

在传统的 JavaScript 开发中,查找 DOM 往往是开发人员遇到的第一个头疼的问题,原生的 JavaScript 所提供的 DOM 选择方法并不多,仅仅局限于通过 tag, name, id 等方式来查找,这显然是远远不够的。现在很多浏览器都支持了高级接口:querySelectorquerySelectorAll

高级接口

querySelectorquerySelectorAll 的使用非常的简单,它和 CSS 的写法完全一样,使用方式几乎和jq的选择器一样。 Document.querySelectorAll()

返回当前文档中匹配一个特定选择器的所有的元素(使用深度优先,前序遍历规则这样的规则遍历所有文档节点) .返回的对象类型是 NodeList.

它的返回值是一个元素的dom的列表,语法如下:

elementList = document.querySelectorAll(selectors);
  • elementList 是一个non-live的 NodeList 类型的对象.
  • selectors 是一个由逗号连接的包含一个或多个CSS选择器的字符串.

Document.querySelector()方法返回第一个。虽然不是所有浏览器都支持这种高级接口,但复杂的选择器尽量走querySelectorAll。具体还是得看看jq里的代码。

Sizzle

此引擎也是jq作者的作品,独立的一个模块,可以单独拿出来用,它几乎支持所有的css,还支持css3,而且很高效。Sizzle也是遵循从右到左开始查找。

div > div.article input[name=ttt]

这个组合的意思是选择所有div的子元素之后的所有

并且class="article" 的所有input并且属性[name=ttt]的元素。高级浏览器可以用原生高级接口,而低版本的就不行了。Sizzle就要利用浏览器基本接口对这个组合进行处理。

节点间的关系

在CSS选择器里边分别是用:空格、>、+、~。表示一个节点和另一个节点的关系。

  ">": { dir: "parentNode", first: true },//祖宗和后代
  " ": { dir: "parentNode" },//父亲和儿子
  "+": { dir: "previousSibling", first: true },//临近兄弟
  "~": { dir: "previousSibling" } //普通兄弟

浏览器提供了四个基本API:

  1. getElementById,上下文只能是HTML文档。
  2. getElementsByName,上下文只能是HTML文档。
  3. getElementsByTagName,上下文可以是HTML文档,XML文档及元素节点。
  4. getElementsByClassName,上下文可以是HTML文档及元素节点。IE8还没有支持。

所以通过Sizzle降级处理,那么内部会有一个规则把选择器分组groups,然后通过从右边往左边查找,加入编译函数的方式节约重复查找的性能问题。

Sizzle选择器引擎的主要工作就是向上兼容querySelectorAll这个API,假如所有浏览器都支持该API,那Sizzle就没有存在的必要性了。

Sizzle词法解析

Sizzle引入了编译的概念,节约重复查到的性能问题。它的token格式如下:

{
   value:'匹配到的字符串', 
   type:'对应的Token类型',
   matches:'正则匹配到的一个结构'
}

假设传入进来的选择器是:div > p + .aaron[type="checkbox"], #id:first-child,解析分组为:

groups: [
  tokens: {
    matches: ? type : ? value : ?
  },
  tokens: {
    matches: ? type : ? value : ?
  }
]

它的构造为:

//函数返回一个token序列:{value:'匹配到的字符串', type:'对应的Token类型', matches:'正则匹配到的一个结构'}
tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
    var matched, match, tokens, type,
        soFar, groups, preFilters,
        cached = tokenCache[ selector + " " ];
//这里的soFar是表示目前还未分析的字符串剩余部分
  //groups表示目前已经匹配到的规则组,在这个例子里边,groups的长度最后是2,存放的是每个规则对应的Token序列
    //如果cache里边有,直接拿出来即可
    if ( cached ) {
        return parseOnly ? 0 : cached.slice( 0 );
    }
    //初始化
    soFar = selector;
    groups = [];//这是最后要返回的结果,一个二维数组
    //比如"title,div > :nth-child(even)"解析下面的符号流
    // [ [{value:"title",type:"TAG",matches:["title"]}],
    //   [{value:"div",type:["TAG",matches:["div"]},
    //    {value:">", type: ">"},
    //    {value:":nth-child(even)",type:"CHILD",matches:["nth",
    //     "child","even",2,0,undefined,undefined,undefined]}
    //   ]
    // ]
    //有多少个并联选择器,里面就有多少个数组,数组里面是拥有value与type的对象

    //这里的预处理器为了对匹配到的Token适当做一些调整
    //自行查看源码,其实就是正则匹配到的内容的一个预处理
    preFilters = Expr.preFilter;
    //递归检测字符串
    //比如"div > p + .aaron input[type="checkbox"]"
    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 = []) );
        }

        matched = false;

        // Combinators
        if ( (match = rcombinators.exec( soFar )) ) {//如果匹配到逗号
            matched = match.shift();////获取到匹配的字符
            //往规则组里边压入一个Token序列,目前Token序列还是空的
            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 ))) ) {
                matched = match.shift();
                //放入Token序列中
                tokens.push({
                    value: matched,
                    type: type,
                    matches: match
                });
                //剩余还未分析的字符串需要减去这段已经分析过的
                soFar = soFar.slice( matched.length );
            }
        }
        //如果到了这里都还没matched到,那么说明这个选择器在这里有错误
        //直接中断词法分析过程
        //这就是Sizzle对词法分析的异常处理
        if ( !matched ) {
            break;
        }
    }

    // Return the length of the invalid excess
    // if we're just parsing
    // Otherwise, throw an error or return tokens
    //放到tokenCache函数里进行缓存
    //如果只需要这个接口检查选择器的合法性,直接就返回soFar的剩余长度,倘若是大于零,说明选择器不合法
    //其余情况,如果soFar长度大于零,抛出异常;否则把groups记录在cache里边并返回,
    return parseOnly ?
        soFar.length :
        soFar ?
            Sizzle.error( selector ) :
            // Cache the tokens
            tokenCache( selector, groups ).slice( 0 );
};

编译

通过tokenize最终分类出来的group分别都有对应的几种type,每一种type都会有对应的处理方法:

Expr.filter = {
    ATTR   : function (name, operator, check) {
    CHILD  : function (type, what, argument, first, last) {
    CLASS  : function (className) {
    ID     : function (id) {
    PSEUDO : function (pseudo, argument) {
    TAG    : function (nodeNameSelector) {
}

例如:

//ID元匹配器工厂
Expr.filter["ID"] =  function( id ) {
    var attrId = id.replace( runescape, funescape );
    //生成一个匹配器
    return function( elem ) {
        var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
        //去除节点的id,判断跟目标是否一致
        return node && node.value === attrId;
    };
};

以及:

//属性元匹配器工厂
//name :属性名
//operator :操作符
//check : 要检查的值
//例如选择器 [type="checkbox"]中,name="type" operator="=" check="checkbox"
"ATTR": function(name, operator, check) {
    //返回一个元匹配器
    return function(elem) {
        //先取出节点对应的属性值
        var result = Sizzle.attr(elem, name);

         //看看属性值有木有!
        if (result == null) {
            //如果操作符是不等号,返回真,因为当前属性为空 是不等于任何值的
            return operator === "!=";
        }
        //如果没有操作符,那就直接通过规则了
        if (!operator) {
            return true;
        }

        result += "";

        //如果是等号,判断目标值跟当前属性值相等是否为真
        return operator === "=" ? result === check :
           //如果是不等号,判断目标值跟当前属性值不相等是否为真
            operator === "!=" ? result !== check :
            //如果是起始相等,判断目标值是否在当前属性值的头部
            operator === "^=" ? check && result.indexOf(check) === 0 :
            //这样解释: lang*=en 匹配这样 <html lang="xxxxenxxx">的节点
            operator === "*=" ? check && result.indexOf(check) > -1 :
            //如果是末尾相等,判断目标值是否在当前属性值的末尾
            operator === "$=" ? check && result.slice(-check.length) === check :
            //这样解释: lang~=en 匹配这样 <html lang="zh_CN en">的节点
            operator === "~=" ? (" " + result + " ").indexOf(check) > -1 :
            //这样解释: lang=|en 匹配这样 <html lang="en-US">的节点
            operator === "|=" ? result === check || result.slice(0, check.length + 1) === check + "-" :
            //其他情况的操作符号表示不匹配
            false;
    };
},

编译函数:

//编译函数机制
//通过传递进来的selector和match生成匹配器:
compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
    var i,
        setMatchers = [],
        elementMatchers = [],
        cached = compilerCache[ selector + " " ];

    if ( !cached ) {//依旧看看有没有缓存
        // Generate a function of recursive functions that can be used to check each element
        if ( !match ) {//如果没有词法解析过
            match = tokenize( selector );
        }
        i = match.length;//从后开始生成匹配器
        //如果是有并联选择器这里多次等循环
        while ( i-- ) {
            cached = matcherFromTokens( match[i] );//这里用matcherFromTokens来生成对应Token的匹配器
            if ( cached[ expando ] ) {
                setMatchers.push( cached );
            } else {//普通的那些匹配器都压入了elementMatchers里边
                elementMatchers.push( cached );
            }
        }

        // Cache the compiled function
        // 这里可以看到,是通过matcherFromGroupMatchers这个函数来生成最终的匹配器
        cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );

        // Save selector and tokenization
        cached.selector = selector;
    }
    //把这个终极匹配器返回到select函数中
    return cached;
};

关于Sizzle的分析这里有份注解挺不错的:http://www.cnblogs.com/mw666666/archive/2013/04/15/3023169.html ,选择器引擎是个独立的部分,我看的头也大了,先跳过了。


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

赏个馒头吧