jQuery 2.0.3 源码分析Sizzle引擎 - 解析原理

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介:

声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢!

先来回答博友的提问:

如何解析

div > p + div.aaron input[type="checkbox"]

顺便在深入理解下解析的原理:

HTML结构

复制代码
<div id="text">
  <p>
     <input type="text" />
  </p>
  <div class="aaron">
     <input type="checkbox" name="readme" value="Submit" />
     <p>Sizzle</p>
  </div>
</div>
复制代码

选择器语句

div > p + div.aaron input[type="checkbox"]

组合后的意思大概就是:

1. 选择父元素为 <div> 元素的所有子元素 <p> 元素

2. 选择紧接在 <p> 元素之后的所有 <div> 并且class="aaron " 的所有元素

3. 之后选择 div.aaron 元素内部的所有 input并且带有 type="checkbox" 的元素

就针对这个简单的结构,我们实际中是不可能这么写的,但是这里我用简单的结构,描述出复杂的处理

我们用组合语句,jquery中,在高级浏览器上都是用过querySelectorAll处理的,所以我们讨论的都是在低版本上的实现,伪类选择器,XML 要放到后最后,本文暂不涉及这方便的处理.

 

需要用到的几个知识点:

1: CSS选择器的位置关系

2: CSS的浏览器实现的基本接口

3: CSS选择器从右到左扫描匹配

 


CSS选择器的位置关系

文档中的所有节点之间都存在这样或者那样的关系

image

其实不难发现,一个节点跟另一个节点有以下几种关系:

祖宗和后代

父亲和儿子   

临近兄弟

普通兄弟

在CSS选择器里边分别是用:空格;>;+;~

(其实还有一种关系:div.aaron,中间没有空格表示了选取一个class为aaron的div节点)

复制代码
<div id="grandfather">
  <div id="father">
    <div id="child1"></div>
    <div id="child2"></div>
    <div id="child3"></div>
  </div>
</div>
复制代码
  • 爷爷grandfather与孙子child1属于祖宗与后代关系(空格表达)
  • 父亲father与儿子child1属于父子关系,也算是祖先与后代关系(>表达)
  • 哥哥child1与弟弟child2属于临近兄弟关系(+表达)
  • 哥哥child1与弟弟child2,弟弟child3都属于普通兄弟关系(~表达)

 

在Sizzle里有一个对象是记录跟选择器相关的属性以及操作:Expr。它有以下属性:

复制代码
relative = {
  ">": { dir: "parentNode", first: true },
  " ": { dir: "parentNode" },
  "+": { dir: "previousSibling", first: true },
  "~": { dir: "previousSibling" }
}
复制代码

所以在Expr.relative里边定义了一个first属性,用来标识两个节点的“紧密”程度,例如父子关系和临近兄弟关系就是紧密的。在创建位置匹配器时,会根据first属性来匹配合适的节点。

 


CSS的浏览器实现的基本接口

除去querySelector,querySelectorAll

HTML文档一共有这么四个API:

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

所以要兼容的话sizzle最终只会有三种完全靠谱的可用

Expr.find = {
      'ID'    : context.getElementById,
      'CLASS' : context.getElementsByClassName,
      'TAG'   : context.getElementsByTagName
}

 


CSS选择器从右到左扫描匹配

接下我们就开始分析解析规则了

1. 选择器语句

div > p + div.aaron input[type="checkbox"]

2. 开始通过词法分析器tokenize分解对应的规则(这个上一章具体分析过了)

复制代码
分解每一个小块
type: "TAG"
value: "div" 
matches ....

type: ">"
value: " > "

type: "TAG"
value: "p"
matches ....

type: "+"
value: " + "

type: "TAG"
value: "div"
matches ....

type: "CLASS"
value: ".aaron"
matches ....

type: " "
value: " "

type: "TAG"
value: "input"
matches ....

type: "ATTR"
value: "[type="checkbox"]"
matches ....

除去关系选择器,其余的有语意的标签都都对应这分析出matches

比如
最后一个属性选择器分支
"[type="checkbox"]"

matches = [
   0: "type"
   1: "="
   2: "checkbox"
]
type: "ATTR" 
value: "[type="checkbox"]"
复制代码

所以就分解出了9个部分了

那么如何匹配才是最有效的方式?

3. 从右往左匹配

最终还是通过浏览器提供的API实现的, 所以Expr.find就是最终的实现接口了

首先确定的肯定是从右边往左边匹配,但是右边第一个是

"[type="checkbox"]"

很明显Expr.find 中不认识这种选择器,所以只能在往前扒一个

趴到了

type: "TAG"
value: "input"

这种标签Expr.find能匹配到了,所以直接调用

复制代码
Expr.find["TAG"] = support.getElementsByTagName ?
    function(tag, context) {
        if (typeof context.getElementsByTagName !== strundefined) {
            return context.getElementsByTagName(tag);
        }
} :
复制代码

但是getElementsByTagName方法返回的是一个合集

所以

这里引入了seed - 种子合集(搜索器搜到符合条件的标签),放入到这个初始集合seed中

OK了 这里暂停了,不在往下匹配了,在用这样的方式往下匹配效率就慢了


开始整理:

重组一下选择器,剔掉已经在用于处理的tag标签,input

所以选择器变成了:

selector: "div > p + div.aaron [type="checkbox"]"

这里可以优化下,如果直接剔除后,为空了,就证明满足了匹配要求,直接返回结果了

到这一步为止

我们能够使用的东东:

1 seed合集

image

2 通过tokenize分析解析规则组成match合集

本来是9个规则快,因为匹配input,所以要对应的也要踢掉一个所以就是8个了

3 选择器语句,对应的踢掉了input

"div > p + div.aaron [type="checkbox"]"

此时send目标合集有2个最终元素了

那么如何用最简单,最有效率的方式从2个条件中找到目标呢?

 

涉及的源码:

复制代码
//引擎的主要入口函数
    function select(selector, context, results, seed) {
        var i, tokens, token, type, find,
            //解析出词法格式
            match = tokenize(selector);

        if (!seed) { //如果外界没有指定初始集合seed了。
            // Try to minimize operations if there is only one group
            // 没有多组的情况下
            // 如果只是单个选择器的情况,也即是没有逗号的情况:div, p,可以特殊优化一下
            if (match.length === 1) {

                // Take a shortcut and set the context if the root selector is an ID
                tokens = match[0] = match[0].slice(0); //取出选择器Token序列

                //如果第一个是selector是id我们可以设置context快速查找
                if (tokens.length > 2 && (token = tokens[0]).type === "ID" &&
                    support.getById && context.nodeType === 9 && documentIsHTML &&
                    Expr.relative[tokens[1].type]) {

                    context = (Expr.find["ID"](token.matches[0].replace(runescape, funescape), context) || [])[0];
                    if (!context) {
                        //如果context这个元素(selector第一个id选择器)都不存在就不用查找了
                        return results;
                    }
                    //去掉第一个id选择器
                    selector = selector.slice(tokens.shift().value.length);
                }

                // Fetch a seed set for right-to-left matching
                //其中: "needsContext"= new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
                //即是表示如果没有一些结构伪类,这些是需要用另一种方式过滤,在之后文章再详细剖析。
                //那么就从最后一条规则开始,先找出seed集合
                i = matchExpr["needsContext"].test(selector) ? 0 : tokens.length;

                //从右向左边查询
                while (i--) { //从后开始向前找!
                    token = tokens[i]; //找到后边的规则

                    // Abort if we hit a combinator
                    // 如果遇到了关系选择器中止
                    //
                    //  > + ~ 空
                    //
                    if (Expr.relative[(type = token.type)]) {
                        break;
                    }

                    /*
                  先看看有没有搜索器find,搜索器就是浏览器一些原生的取DOM接口,简单的表述就是以下对象了
                  Expr.find = {
                    'ID'    : context.getElementById,
                    'CLASS' : context.getElementsByClassName,
                    'NAME'  : context.getElementsByName,
                    'TAG'   : context.getElementsByTagName
                  }
                */
                    //如果是:first-child这类伪类就没有对应的搜索器了,此时会向前提取前一条规则token
                    if ((find = Expr.find[type])) {

                        // Search, expanding context for leading sibling combinators
                        // 尝试一下能否通过这个搜索器搜到符合条件的初始集合seed
                        if ((seed = find(
                            token.matches[0].replace(runescape, funescape),
                            rsibling.test(tokens[0].type) && context.parentNode || context
                        ))) {

                            //如果真的搜到了
                            // If seed is empty or no tokens remain, we can return early
                            //把最后一条规则去除掉
                            tokens.splice(i, 1);
                            selector = seed.length && toSelector(tokens);

                            //看看当前剩余的选择器是否为空
                            if (!selector) {
                                //是的话,提前返回结果了。
                                push.apply(results, seed);
                                return results;
                            }

                            //已经找到了符合条件的seed集合,此时前边还有其他规则,跳出去
                            break;
                        }
                    }
                }
            }
        }


        // "div > p + div.aaron [type="checkbox"]"

        // Compile and execute a filtering function
        // Provide `match` to avoid retokenization if we modified the selector above
        // 交由compile来生成一个称为终极匹配器
        // 通过这个匹配器过滤seed,把符合条件的结果放到results里边
        //
        //    //生成编译函数
        //  var superMatcher =   compile( selector, match )
        //
        //  //执行
        //    superMatcher(seed,context,!documentIsHTML,results,rsibling.test( selector ))
        //
        compile(selector, match)(
            seed,
            context, !documentIsHTML,
            results,
            rsibling.test(selector)
        );
        return results;
    }
复制代码

 

这个过程在简单总结一下

复制代码
selector:"div > p + div.aaron input[type="checkbox"]"

解析规则:
1 按照从右到左
2 取出最后一个token  比如[type="checkbox"]
                            {
                                matches : Array[3]
                                type    : "ATTR"
                                value   : "[type="
                                checkbox "]"
                            }
3 过滤类型 如果type是 > + ~ 空 四种关系选择器中的一种,则跳过,在继续过滤
4 直到匹配到为 ID,CLASS,TAG  中一种 , 因为这样才能通过浏览器的接口索取
5 此时seed种子合集中就有值了,这样把刷选的条件给缩的很小了
6 如果匹配的seed的合集有多个就需要进一步的过滤了,修正选择器 selector: "div > p + div.aaron [type="checkbox"]"
7 OK,跳到一下阶段的编译函数
复制代码

 

Sizzle不仅仅是简简单单的从右往左匹配的

本文转自艾伦 Aaron博客园博客,原文链接:http://www.cnblogs.com/aaronjs/p/3310937.html,如需转载请自行联系原作者

相关文章
|
8天前
|
存储 缓存 算法
HashMap深度解析:从原理到实战
HashMap,作为Java集合框架中的一个核心组件,以其高效的键值对存储和检索机制,在软件开发中扮演着举足轻重的角色。作为一名资深的AI工程师,深入理解HashMap的原理、历史、业务场景以及实战应用,对于提升数据处理和算法实现的效率至关重要。本文将通过手绘结构图、流程图,结合Java代码示例,全方位解析HashMap,帮助读者从理论到实践全面掌握这一关键技术。
46 13
|
26天前
|
运维 持续交付 云计算
深入解析云计算中的微服务架构:原理、优势与实践
深入解析云计算中的微服务架构:原理、优势与实践
59 1
|
1月前
|
机器学习/深度学习 安全 大数据
揭秘!企业级大模型如何安全高效私有化部署?全面解析最佳实践,助你打造智能业务新引擎!
【10月更文挑战第24天】本文详细探讨了企业级大模型私有化部署的最佳实践,涵盖数据隐私与安全、定制化配置、部署流程、性能优化及安全措施。通过私有化部署,企业能够完全控制数据,确保敏感信息的安全,同时根据自身需求进行优化,提升计算性能和处理效率。示例代码展示了如何利用Python和TensorFlow进行文本分类任务的模型训练。
104 6
|
2月前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
56 3
|
1天前
|
设计模式 XML Java
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
本文详细介绍了Spring框架的核心功能,并通过手写自定义Spring框架的方式,深入理解了Spring的IOC(控制反转)和DI(依赖注入)功能,并且学会实际运用设计模式到真实开发中。
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
|
2天前
|
网络协议 安全 网络安全
探索网络模型与协议:从OSI到HTTPs的原理解析
OSI七层网络模型和TCP/IP四层模型是理解和设计计算机网络的框架。OSI模型包括物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,而TCP/IP模型则简化为链路层、网络层、传输层和 HTTPS协议基于HTTP并通过TLS/SSL加密数据,确保安全传输。其连接过程涉及TCP三次握手、SSL证书验证、对称密钥交换等步骤,以保障通信的安全性和完整性。数字信封技术使用非对称加密和数字证书确保数据的机密性和身份认证。 浏览器通过Https访问网站的过程包括输入网址、DNS解析、建立TCP连接、发送HTTPS请求、接收响应、验证证书和解析网页内容等步骤,确保用户与服务器之间的安全通信。
17 1
|
1月前
|
运维 持续交付 虚拟化
深入解析Docker容器化技术的核心原理
深入解析Docker容器化技术的核心原理
47 1
|
1月前
|
Kubernetes Cloud Native 调度
云原生批量任务编排引擎Argo Workflows发布3.6,一文解析关键新特性
Argo Workflows是CNCF毕业项目,最受欢迎的云原生工作流引擎,专为Kubernetes上编排批量任务而设计,本文主要对最新发布的Argo Workflows 3.6版本的关键新特性做一个深入的解析。
|
27天前
|
存储 供应链 算法
深入解析区块链技术的核心原理与应用前景
深入解析区块链技术的核心原理与应用前景
52 0
|
1月前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
60 1

推荐镜像

更多