Ruby's Louvre

每天学习一点点算法

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

Sizzle是怎样工作的

作为闻名遐迩的选择器,Sizzle并不是人们所说的从右到左选择那么简单的。本文将揭开其神秘的内幕,让大家看看其令人惊叹的优化。

在此之前,大家必需有一些概念,否则无法勾勒其轮廓。这些概念我在做类似的事,一点点总结出来的。总的来说,有如下几个:种子选择器,候选集,映射集,迭代器,过滤器,切割器,查询次数……

由于CSS选择器的种类是如此繁多,加上jQuery的自定义伪类就更多,因此它们组成的表达式一定要加以拆分,才能工作。我们就拿一个简单的CSS表达式着手吧。

div div.aaa就是我们的小白鼠,大抵它可以拆分为四个部分,标签选择器,后代选择器,标签选择器与类选择器。但Sizzle一开始并没有这样做,请见下面Sizzle的切割器部分

parts = [],
   soFar = selector;
  //chunker 的正则会用不在双引号单引号之内的后代亲子相邻兄长并联(' ','>','+','~',',')作为切割符,
  //从左到右切割表达式,并在第一个并联选择器之时结束。
  //比如"li[name=ee]>span.aaa, div span" 会变成parts = ["li[name=ee]",">","span.aaa"], extra = "div span"
   do {
       chunker.exec( "" );
       m = chunker.exec( soFar );
       if ( m ) {
           soFar = m[3];
           parts.push( m[1] );
           if ( m[2] ) {//如果是并联选择器,中断
                extra = m[3];
               break;
           }
       }
   } while ( m );

单从这点,这种切割方法就注定其不可能是单纯的从右到左选择,它可能是从中间到左进行。

接下来有许多分支,分别是对ID与位置伪类进行优化的,我们暂时跳过它们。看其另外几个重要概念,种子选择器与候选集,候选集你也可以称之为种子集,因为sizzle中就有这么一个单词seed。为了获取候选集,我们需要调一个种子选择器Sizzle.find。重要的参数是并联选择器前面的那个子选择器模块,若根据上面的例子,它就是那个"span.aaa"。"span.aaa"的组成部分在标准浏览器下都可以用原生API来获取。除去后来的那两个querySelector,querySelectorAll,HTML文档一共有这么四个API:

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

那我们是先取span还是.aaa,就要视原生API获取元素节点的数量而定,更直白地说,数量越少,后面过滤起来越轻松,John Resig不是傻瓜,肯定在这里做点优化。这个原生选择器的调用顺序被放到一个叫Sizzle.selectors.order的数组中,对于旧一点的浏览器,其顺序为ID,NAME,TAG,对于支持getElementsByClassName的浏览器,其顺序为ID,CLASS,NAME,TAG。种子选择器Sizzle.find会调用相应的正则去切割它,并进行进一步的处理放进原生API中去获取节点。当然不如意的情况还有许多,如碰到选择器群组是"[href='aaa']:target"的情况,我们唯有把文档的所有节点充当获选集了。

Sizzle.find = function( expr, context, isXML ) {
    var set;
    if ( !expr ) {
        return [];
    }
    for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
        var match,type = Expr.order[i];
        if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
            var left = match[1];
            match.splice( 1, 1 );
            if ( left.substr( left.length - 1 ) !== "\\" ) {
                match[1] = (match[1] || "").replace(/\\/g, "");
                set = Expr.find[ type ]( match, context, isXML );
 
                if ( set != null ) {
                    expr = expr.replace( Expr.match[ type ], "" );
                    break;
                }
            }
        }
    }
    if ( !set ) {
        set = context.getElementsByTagName( "*" );
    }
    return { set: set, expr: expr };
};

经过种子选择器后,原选择器群组可能还留下一点小尾巴,如上面的例子,在FF会留下"span"。这时轮到五大迭代器出场了。在我的体系中,我分别把它们取名为border,borders,father,fathers,current,在Sizzle里具体对应Sizzle.selectors.relative["+"],Sizzle.selectors.relative["~"],Sizzle.selectors.relative[">"],Sizzle.selectors.relative[""]及Sizzle.filter。Sizzle.filter在father与fathers还充当过滤器。我们还是先看current迭代器吧。current迭代器的意思应该很易懂,只对于当前集合的元素进行或过滤或置换处理。

Sizzle.filter = function( expr, set, inplace, not ) {
//set必须是数组,inplace为true时用于其他迭代器,not只用于最简单的反选选择器
    var match, anyFound,
        old = expr,
        result = [],
        curLoop = set,
        isXMLFilter = set && set[0] && Sizzle.isXML( set[0] );
    while ( expr && set.length ) {
        for ( var type in Expr.filter ) {
//这里是除关系选择器与并联选择器的其他选择器类型,其中还把位置伪类从伪类中独立出来
//CLASS,ID,TAG,CHILD,ATTR,PSEUDO,POS
            if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {//取得过滤参数
                var found, item,
                    filter = Expr.filter[ type ],//取得过滤函数
                    left = match[1];
                anyFound = false;
                match.splice(1,1);
                if ( left.substr( left.length - 1 ) === "\\" ) {
                    continue;
                }
                if ( curLoop === result ) {
                    result = [];//这里用于缩小候选集时
                }
                if ( Expr.preFilter[ type ] ) {
               //Expr.preFilter里面的函数都用于进一步加工过滤参数,对于类选择器,出于优化需要,直接进行过滤(它返回false)
                    match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
                    if ( !match ) {//处理CLASS
                        anyFound = found = true;
                    } else if ( match === true ) {//处理POS
                        continue;
                    }
                }
                if ( match ) {//处理CLASS、POS之外的类型
                    for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
                        if ( item ) {//如果item不为false
                            found = filter( item, match, i, curLoop );//此值为布尔
                            var pass = not ^ !!found;//此值为布尔
 
                            if ( inplace && found != null ) {
                                if ( pass ) {
                                    anyFound = true;
 
                                } else {
                                    curLoop[i] = false;
                                }
 
                            } else if ( pass ) {
                                result.push( item );//这里用于构建一下循环的数组
                                anyFound = true;
                            }
                        }
                    }
                }
                if ( found !== undefined ) {
                    if ( !inplace ) {
                        curLoop = result;
                    }
                    expr = expr.replace( Expr.match[ type ], "" );//削减CSS表达式直到变为空字符串
                    if ( !anyFound ) {
                        return [];
                    }
 
                    break;
                }
            }
        }
        // Improper expression
        if ( expr === old ) {//如果到最后正则也不能动CSS表达式一条毫毛,说明此CSS表达式有问题
            if ( anyFound == null ) {
                Sizzle.error( expr );
 
            } else {
                break;
            }
        }
        old = expr;
    }
    return curLoop;//这里用于其他迭代器
};

嘛,简单来说,此迭代器有两种形态。一是当inplace为undefined时,它会不断缩减集合的数量。如[ [object HTMLDivElement], [object HTMLDivElement], [object HTMLDivElement], [object HTMLDivElement] ]缩减成[ [object HTMLDivElement] ]一个,被操作的集合为候选集本身。另一种是inplace为true时,它不会缩减集合的数量,但会将不符合的节点置换为false,若在fathers迭代器中,符合条件的节点还会变成其某一个祖先,borders则变成其某一个previousSiblingElement,border与father相仿,总之会面目全非。这种情况是应用于映射集,映射集与候选集的数量总是相等。在Sizzle,映射集的变量名为checkSet。

好了,总之到下面这一段代码,span选择器已被搞掉了。

set = ret.expr ?Sizzle.filter( ret.expr, ret.set ) :ret.set;

这时候候选集的数量已少到不能最少,我们需要设置一个映射集:

checkSet = makeArray( set );

现在我们就来处理其他子选择器群集,它们都是以关系选择器间隔开的。

while ( parts.length ) {
  cur = parts.pop();//取得关系选择器
  pop = cur;
  if ( !Expr.relative[ cur ] ) {
    cur = "";//如果不是则默认为后代选择器
  } else {
    pop = parts.pop();//取得后代选择器前面的子选择器群集
  }
  if ( pop == null ) {
    pop = context;
  }
  Expr.relative[ cur ]( checkSet, pop, contextXML );//根据其他四种迭代器改变映射集里面的元素
//得到诸如[ [object HTMLDivElement] ,false, false, [object HTMLSpanElement] ]的集合
}

最后一步就是根据映射集甄选候选集。

for ( i = 0; checkSet[i] != null; i++ ) {
                  if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
                      results.push( set[i] );
                  }
 }

如果存在并联选择器,那就再调用Sizzle主函数,把得到的两个结果合并去重就是。

嘛,以上就是Sizzle的基本流程了,其他迭代器,过滤器的解读应该没有难度,有空再说。下再重新把大纲罗列一下结文:

  • 将CSS表达式切割到第一个并联选择器为止,得到的数组是按子选择器群集,关系选择器,子选择器群组,关系选择器.....的顺序排列。
  • 将最后一个选择器放进种子选择器(Sizzle.find),得到最佳的候选集。
  • 如果刚才的子选择器群组还残留什么,就放到current迭代器,把候选集的元素个数缩小(减少过滤基数)。
  • 然后根据候选集克隆一个映射集,用于放进其他迭代器的过滤函数中进行筛选,不符合者将置换为false,符合者置换其兄长祖先。
  • 最后根据映射集甄选候选集,如果存在并联选择器,重复刚才的事,两者合并排序去重返回。

如果您觉得此文有帮助,可以打赏点钱给我支付宝1669866773@qq.com ,或扫描二维码

posted on   司徒正美  阅读(5843)  评论(6编辑  收藏  举报

编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· SQL Server 2025 AI相关能力初探
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
点击右上角即可分享
微信分享提示