浅谈Sizzle的“编译原理”


Posted in Javascript onApril 14, 2015

Sizzle,是jQuery作者John Resig写的DOM选择器引擎,速度号称业界第一。作为一个独立全新的选择器引擎,出现在jQuery 1.3版本之后,并被John Resig作为一个开源的项目。Sizzle是独立的一部分,不依赖任何库,如果你不想用jQuery,可以只用Sizzle,也可以用于其他框架如:Mool, Dojo,YUI等。

前几天在准备一个关于jQuery的分享PPT,问同事关于jQuery除了使用方法之外还有没有其他特别想了解一下的,有人提到了想了解下它的选择器是怎么实现的,也有人提到jQuery的查询速度跟其他框架比怎么样。关于速度,sizzle的官方网站上可以下载测试的例子,速度确实很有优势。但是它为什么会有这样高效的运行速度,就跟这里想探讨的实现原理有关系了。

在了解Sizzle之前必须要先了解它的选择器是怎么回事,这里有一个简单的例子,熟悉jQuery的同学也一定很熟悉这样的选择器格式:

tag #id .class , a:first

它基本上是从左到右层层深入过滤去查找匹配的dom元素,这个语句还不算复杂。假设我们自己来实现这一条查询语句的话,也不难。但是,查询语句只有基本的规则,没有固定的选择符个数和顺序,我们自己写代码怎样能适应这种随意的排列组合?Sizzle就能做到各种情况的正常解析、执行。

Sizzle的源码确实错综复杂不容易理清楚它的思路。先抛开外面层层的包裹,直接看看我个人认为整个实现里很核心的三个方法:

第一个核心方法。源码第1052行有一个tokenize函数:

function tokenize(selector, parseOnly ) { }

第二个参数parseOnly为false的意思是只做token序列化操作,不返回结果,这个情况下序列化的结果会被缓存起来备用。Selector就是查询语句了。

经过这个函数处理后,比如selector="#idtag.class , a:first"传进去,可以得到一个格式类似于下面的结果:

[
[
{matches:" id ",type:"ID"},
{matches:" tag ",type:"TAG"},
{matches:" class ",type:"CLASS"},
...
],
[
    {matches:" a",type:"TAG"},
    ...
],
[…],
…
]

看到tokenize这个函数的命名和它的作用,让我很容易就联想起“编译原理”这个词了。这里就有点像是词法分析了,不过这个词法分析比程序编译时做的词法分析简单。

tokenize方法会根据selector里面的逗号,空格和关系选择符的正则表达式做“分词”,得到一个二维数组(请允许我冒用这个不是很准确的称呼),其中第一维数组是根据逗号分隔出来的,在源代码里面被称作groups。

我们再看源代码第405行开始有一个Expr = Sizzle.selectors = {}的定义,其中到567行的时候有一个filter的定义,这里我们能找到基本的过滤类型:"ID"、"TAG"、"CLASS"、"ATTR"、"CHILD"、"PSEUDO",tokenize最终分类出来的type也就是这几种。

“分词”完成之后,依旧看在405行定义的Expr= Sizzle.selectors = {}。这里面能找到我们熟悉的所有选择符,每个选择符对应一个方法定义。到这里应该想到Sizzle其实是不是就是通过对selector做“分词”,打散之后再分别从Expr里面去找对应的方法来执行具体的查询或者过滤的操作?

答案基本是肯定的。但是Sizzle有更具体和巧妙的做法。再来看我认为很核心的第二个方法:

源代码1293行有一个matcherFromTokens函数:

function matcherFromTokens(tokens) { }

传入的参数正是从tokenize方法得到的。Matcher可以理解成是“匹配程序”的意思,光从字面上看这个函数起的作用就是通过tokens生成匹配程序。事实上确实如此。限于篇幅,这篇文章暂且只分享我理解到的一些Sizzle的实现原理,不贴源码。后面有时间我或者再整理一篇更详尽的源码分析的文章。

matcherFromTokens方法证实了前面的设想,它充当了selector“分词”与Expr中定义的匹配方法的串联与纽带的作用,可以说选择符的各种排列组合都是能适应的了。Sizzle巧妙的就是它没有直接将拿到的“分词”结果与Expr中的方法逐个匹配逐个执行,而是先根据规则组合出一个大的匹配方法,最后一步执行。但是组合之后怎么执行的,还得再看关键的第三个方法:

源代码1350行有一个superMatcher方法:

superMatcher = function( seed, context, xml, results, expandContext ) { }

这个方法并不是一个直接定义的方法,而是通过1345行的matcherFromGroupMatchers( elementMatchers, setMatchers )方法return出来的,但是最后执行起重要作用的是它。

superMatcher方法会根据参数seed、expandContext和context确定一个起始的查询范围,有可能是直接从seed中查询过滤,也有可能在context或者context的父节点范围内。如果不是从seed开始,那么它会先执行Expr.find["TAG"]( "*", expandContext && context.parentNode || context )这句代码等到一个elems集合(数组)。然后对elems做一个遍历,对里面的元素逐个使用预先生成的matcher方法做匹配,如果结果为true的则直接将元素堆入返回结果集里面。

好吧,看到这里matcher方法原来运行的结果都是bool值,我们再返回405行看一下Expr里面filter包含的方法,都是返回bool值的。包括PSEUDO(伪类)对应的更多的伪类方法都一样。似乎有点颠覆我最初的设想,它原来不是一层一层往下查,却有点倒回去向上做匹配、过滤的意思。Expr里面只有find和preFilter返回的是集合。

尽管到这里暂时还带着一点疑问,就是最后它为什么用的是逐个匹配、过滤的方法来得到结果集,但是我想Sizzle最基本的“编译原理”应该已经解释清楚了。

但是疑问不能留着,我们继续。其实这篇文章本身已经有点倒过来写的味道了。有兴趣看源码的同学不会一开始就看到这三个关键的方法。实际上Sizzle在进入这三个方法之前,还做了一系列的其他工作。

Sizzle的真正入口可以说是在源码的220行:

function Sizzle( selector, context, results, seed ){ }

这个方法前面一段比较容易懂,如果能匹配到selector是单一的ID选择符(#id),则先根据id就直接用context.getElementById( m )方法把元素找出来。如果能匹配到selector是单一的TAG选择符,也先直接用context.getElementsByTagName( selector )方法把相关的元素找出来。如果当前浏览器只是原生的getElementsByClassName,并且匹配到selector是单一的CLASS选择符,也会也用context.getElementsByClassName( m )方法把相关的元素找出来。这个三个方法,都是浏览器支持的原生方法,执行效率肯定是最高的。

如果最基本的方法都用不上的话,才会进入到select方法。源码1480行有它的定义:

function select( selector, context, results, seed, xml ) { }

在select方法里面,首先会对selector做我们前面提到的“分词”操作。但是这个操作之后并没有直接开始组装匹配方法,而是先做了一些find的操作。这里的find操作就可以对应到Expr里面的find,它执行的是查询操作,返回的是结果集。

可以这样理解,select利用“分词”得到的选择符根据它的type先将可以用find方法查找的结果集查出来。做find操作的时候,是按照选择符的顺序从左到右缩小结果集范围的。如果一个遍历下来,selector中的所有选择符都可以执行find操作,则直接将结果返回。否则,就进入前面介绍的“编译”执行过滤的流程了。

到这里,也可以顺过来,基本上理清楚Sizzle的工作流程了。前面留下的疑问到此时其实也不算疑问了,因为执行反向匹配过滤的时候,它的查找范围已经是经过层层过滤的最小集合了。而反向匹配过滤的方法对于它所对应的那些选择符,比如伪类之类的,其实也已经是一个高效的选择。

再来简单总结为什么Sizzle很高效。

首先,从处理流程上,它总是先使用最高效的原生方法来做处理。前面一直在介绍的还只是Sizzle自身的选择器实现方法,真正Sizzle执行的时候,它还会先判断当前浏览器是否支持querySelectorAll原生方法(源代码1545行)。如果支持的话,则优先选用此方法,浏览器原生支持的方法,效率肯定比Sizzle自己js写的方法要高,优先使用也能保证Sizzle更高的工作效率。(关于querySelectorAll可以上网查阅更多资料)。在不支持querySelectorAll方法的情况下,Sizzle也是优先判断是不是可以直接使用getElementById、getElementsByTag、getElementsByClassName等方法解决问题。

其次,相对复杂的情况,Sizzle总是选择先尽可能利用原生方法来查询选择来缩小待选范围,然后才会利用前面介绍的“编译原理”来对待选范围的元素逐个匹配筛选。进入到“编译”这个环节的工作流程有些复杂,效率相比前面的方法肯定会稍低一些,但Sizzle在努力尽量少用这些方法,同时也努力让给这些方法处理的结果集尽量小和简单,以便获得更高的效率。

再次,即便进入到这个“编译”的流程,Sizzle还做了我们前面为了优先解释清楚流程而暂时忽略、没有介绍的缓存机制。源代码1535行是我们所谓的“编译”入口,也就是它会调用第三个核心方法superMatcher。跟踪进去看1466行,compile方法将根据selector生成的匹配函数缓存起来了。还不止如此,再到1052行看tokenize方法,它其实也将根据selector做的分词结果缓存起来了。也就是说,当我们执行过一次Sizzle (selector)方法以后,下次再直接调用Sizzle (selector)方法,它内部最耗性能的“编译”过程不会再耗太多性能了,直接取之前缓存的方法就可以了。我在想所谓“编译”的最大好处之一可能也就是便于缓存,所谓“编译”在这里可能也就可以理解成是生成预处理的函数存储起来备用。

到此我希望基本能够解答之前关心选择器实现原理和执行效率问题的同学的疑问了。另外本文分析结论源自于Sizzle最新版本源码,文中提到的代码行号以此版本源码为准,可以从http://sizzlejs.com/下载。时间仓促,分析有不周到的地方拍砖请手下留情,还有疑问的同学欢迎线下继续交流。

以上所述就是本文的全部内容了,希望大家能够喜欢。

Javascript 相关文章推荐
dojo 之基础篇
Mar 24 Javascript
js复制到剪切板的实例方法
Jun 28 Javascript
JSON 数字排序多字段排序介绍
Sep 18 Javascript
jQuery$命名冲突怎么办如何解决
Jan 16 Javascript
jquery easyui使用心得
Jul 07 Javascript
使用AngularJS实现表单向导的方法
Jun 19 Javascript
JS 实现计算器详解及实例代码(一)
Jan 08 Javascript
AngulerJS学习之按需动态加载文件
Feb 13 Javascript
微信小程序实现页面跳转传值的方法
Oct 12 Javascript
详解组件库的webpack构建速度优化
Jun 18 Javascript
Bootstarp在pycharm中的安装及简单的使用方法
Apr 19 Javascript
JS数组reduce()方法原理及使用技巧解析
Jul 14 Javascript
深入探寻seajs的模块化与加载方式
Apr 14 #Javascript
javascript数组去重的方法汇总
Apr 14 #Javascript
JavaScript字符串常用类使用方法汇总
Apr 14 #Javascript
JavaScript 表单处理实现代码
Apr 13 #Javascript
JavaScript 事件绑定及深入
Apr 13 #Javascript
JavaScript 事件对象介绍
Apr 13 #Javascript
JavaScript 事件入门知识
Apr 13 #Javascript
You might like
PHP 引用文件技巧
2010/03/02 PHP
thinkphp实现多语言功能(语言包)
2014/03/04 PHP
推荐十款免费 WordPress 插件
2015/03/24 PHP
PHP解压tar.gz格式文件的方法
2016/02/14 PHP
json原理分析及实例介绍
2012/11/29 Javascript
js实现日历可获得指定日期周数及星期几示例分享(js获取星期几)
2014/03/14 Javascript
Javascript实现单张图片浏览
2014/12/18 Javascript
跟我学习JScript的Bug与内存管理
2015/11/18 Javascript
AngularJS页面访问时出现页面闪烁问题的解决
2016/03/06 Javascript
jQuery Tags Input Plugin(添加/删除标签插件)详解
2016/06/20 Javascript
jQuery纵向导航菜单效果实现方法
2016/12/19 Javascript
jQuery插件JWPlayer视频播放器用法实例分析
2017/01/11 Javascript
JS数组去重常用方法实例小结【4种方法】
2018/05/28 Javascript
JS/HTML5游戏常用算法之追踪算法实例详解
2018/12/12 Javascript
vue移动端屏幕适配详解
2019/04/30 Javascript
vue通信方式EventBus的实现代码详解
2019/06/10 Javascript
Swiper.js实现移动端元素左右滑动
2019/09/08 Javascript
详解如何在Vue项目中发送jsonp请求
2019/10/25 Javascript
序列化模块json代码实例详解
2020/03/03 Javascript
JQuery实现折叠式菜单的详细代码
2020/06/03 jQuery
openlayers实现地图测距测面
2020/09/25 Javascript
[09:40]DAC2018 4.5 SOLO赛 MidOne vs Miracle
2018/04/06 DOTA
在Django的URLconf中进行函数导入的方法
2015/07/18 Python
玩转python爬虫之URLError异常处理
2016/02/17 Python
对numpy数据写入文件的方法讲解
2018/07/09 Python
python PrettyTable模块的安装与简单应用
2019/01/11 Python
Django app配置多个数据库代码实例
2019/12/17 Python
解决jupyter notebook import error但是命令提示符import正常的问题
2020/04/15 Python
美国和加拿大房车出售在线分类广告:RVT.com
2018/04/23 全球购物
BannerBuzz加拿大:在线定制横幅印刷、广告和标志
2020/03/10 全球购物
国外软件测试工程师面试题
2016/12/09 面试题
物业经理求职自我评价
2013/09/22 职场文书
班队活动设计方案
2014/01/30 职场文书
国际商贸专业自荐信
2014/06/09 职场文书
父亲节活动策划方案
2014/08/24 职场文书
党的群众路线领导班子整改方案
2014/09/27 职场文书