浅谈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 相关文章推荐
js获取网页高度(详细整理)
Dec 28 Javascript
浅析jQuery EasyUI中的tree使用指南
Dec 18 Javascript
jquery滚动加载数据的方法
Mar 09 Javascript
JS实现控制表格内指定单元格内容对齐的方法
Mar 30 Javascript
jquery实现图片水平滚动效果代码分享
Aug 26 Javascript
js实现简单计算器
Nov 22 Javascript
javaScript知识点总结(必看篇)
Jun 10 Javascript
Angular2库初探
Mar 01 Javascript
JS+canvas实现的五子棋游戏【人机大战版】
Jul 19 Javascript
基于JavaScript实现幸运抽奖页面
Jul 05 Javascript
js中火星坐标、百度坐标、WGS84坐标转换实现方法示例
Mar 02 Javascript
Vue CLI4 Vue.config.js标准配置(最全注释)
Jun 05 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新闻发布系统教程
2014/05/09 PHP
ThinkPHP模板判断输出Empty标签用法详解
2014/06/30 PHP
PHP将字符分解为多个字符串的方法
2014/11/22 PHP
php实现的mongodb操作类实例
2015/04/03 PHP
php实现QQ空间获取当前用户的用户名并生成图片
2015/07/25 PHP
php安装php_rar扩展实现rar文件读取和解压的方法
2016/11/17 PHP
Laravel 之url参数,获取路由参数的例子
2019/10/21 PHP
解决jquery .ajax 在IE下卡死问题的解决方法
2009/10/26 Javascript
javascript showModalDialog,open取得父窗口的方法
2010/03/10 Javascript
javascript中的=等号个数问题两个跟三个有什么区别
2013/10/23 Javascript
jQuery中map()方法用法实例
2015/01/06 Javascript
jQuery使用之设置元素样式用法实例
2015/01/19 Javascript
js验证上传图片的方法
2015/05/12 Javascript
利用js实现禁止复制文本信息
2015/06/03 Javascript
javascript遇到html5的一些表单属性
2015/07/05 Javascript
D3.js实现饼状图的方法详解
2016/09/21 Javascript
Vue2.0使用过程常见的一些问题总结学习
2017/04/10 Javascript
微信小程序 页面跳转传值实现代码
2017/07/27 Javascript
基于vue2的canvas时钟倒计时组件步骤解析
2018/11/05 Javascript
JQuery获取元素尺寸、位置及页面滚动事件应用示例
2019/05/14 jQuery
Vue 实现html中根据类型显示内容
2019/10/28 Javascript
Vue 实现CLI 3.0 + momentjs + lodash打包时优化
2019/11/13 Javascript
JS如何在数组指定位置插入元素
2020/03/10 Javascript
python实现多线程暴力破解登陆路由器功能代码分享
2015/01/04 Python
python如何定义带参数的装饰器
2018/03/20 Python
python2.x实现人民币转大写人民币
2018/06/20 Python
Python模块汇总(常用第三方库)
2019/10/07 Python
python读取与处理netcdf数据方式
2020/02/14 Python
关于torch.optim的灵活使用详解(包括重写SGD,加上L1正则)
2020/02/20 Python
浅谈keras使用预训练模型vgg16分类,损失和准确度不变
2020/07/02 Python
python操作链表的示例代码
2020/09/27 Python
澳大利亚网上买书:Angus & Robertson
2019/07/21 全球购物
护士实习生自我鉴定范文
2013/12/10 职场文书
毕业设计说明书
2014/05/07 职场文书
投标诚信承诺书
2014/05/26 职场文书
Redis 操作多个数据库的配置的方法实现
2022/03/23 Redis