浅谈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 相关文章推荐
基于jQuery的投票系统显示结果插件
Aug 12 Javascript
各浏览器中querySelector和querySelectorAll的实现差异分析
May 23 Javascript
jQuery点击后一组图片左右滑动的实现代码
Aug 16 Javascript
javascript中的if语句使用介绍
Nov 20 Javascript
禁用JavaScript控制台调试的方法
Mar 07 Javascript
javascript实现可拖动变色并关闭层窗口实例
May 15 Javascript
JS+CSS相对定位实现的下拉菜单
Oct 06 Javascript
ionic由于使用了header和subheader导致被遮挡的问题的两种解决方法
Sep 22 Javascript
js实现复制功能(多种方法集合)
Jan 06 Javascript
如何解决日期函数new Date()浏览器兼容性问题
Sep 11 Javascript
使用Vue调取接口,并渲染数据的示例代码
Oct 28 Javascript
vue-router定义元信息meta操作
Dec 07 Vue.js
深入探寻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与MySQL交互使用详解
2006/10/09 PHP
Laravel timestamps 设置为unix时间戳的方法
2019/10/11 PHP
php libevent 功能与使用方法详解
2020/03/04 PHP
capacityFixed 基于jquery的类似于新浪微博新消息提示的定位框
2011/05/24 Javascript
JavaScript高级程序设计(第3版)学习笔记8 js函数(中)
2012/10/11 Javascript
浅谈JavaScript中运算符的优先级
2015/07/07 Javascript
jQuery Ajax Post 回调函数不执行问题的解决方法
2016/08/15 Javascript
基于js实现checkbox批量选中操作
2016/11/22 Javascript
jQuery实现动态文字搜索功能
2017/01/05 Javascript
Angular2开发——组件规划篇
2017/03/28 Javascript
Angular.js去除页面中显示的空行方法示例
2017/03/30 Javascript
使用JavaScript开发跨平台的桌面应用详解
2017/07/27 Javascript
Angular 2 利用Router事件和Title实现动态页面标题的方法
2017/08/23 Javascript
详解用Node.js写一个简单的命令行工具
2018/03/01 Javascript
Vue父子组件之间的通信实例详解
2018/09/28 Javascript
angular4笔记系列之内置指令小结
2018/11/09 Javascript
Vue项目总结之webpack常规打包优化方案
2019/06/06 Javascript
如何优雅地在Node应用中进行错误异常处理
2019/11/25 Javascript
es6函数之严格模式用法实例分析
2020/03/17 Javascript
[51:17]VGJ.T vs Mineski 2018国际邀请赛小组赛BO2 第二场 8.18
2018/08/19 DOTA
[45:16]完美世界DOTA2联赛PWL S3 Magma vs Phoenix 第一场 12.12
2020/12/16 DOTA
详解Python 协程的详细用法使用和例子
2018/06/15 Python
对python指数、幂数拟合curve_fit详解
2018/12/29 Python
详解python uiautomator2 watcher的使用方法
2019/09/09 Python
完美解决Django2.0中models下的ForeignKey()问题
2020/05/19 Python
基于Python pyecharts实现多种图例代码解析
2020/08/10 Python
彻底搞懂python 迭代器和生成器
2020/09/07 Python
PyQt5通过信号实现MVC的示例
2021/02/06 Python
介绍一下结构化程序设计方法和面向对象程序设计方法的区别
2012/06/27 面试题
建筑公司文秘岗位职责
2013/11/29 职场文书
开门红主持词
2014/04/02 职场文书
中国梦口号
2014/06/13 职场文书
美术课外活动总结
2014/07/08 职场文书
社区元宵节活动总结
2015/02/06 职场文书
雷锋观后感
2015/06/10 职场文书
python基于OpenCV模板匹配识别图片中的数字
2021/03/31 Python