浅谈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 相关文章推荐
实现web打印的各种方法介绍及实现代码
Jan 09 Javascript
怎么判断js脚本加载完成
Feb 28 Javascript
JS动态显示表格上下frame的方法
Mar 31 Javascript
JavaScript操作URL的相关内容集锦
Oct 29 Javascript
JavaScript动态创建div等元素实例讲解
Jan 06 Javascript
深入学习jQuery Validate表单验证
Jan 18 Javascript
让DIV的滚动条自动滚动到最底部的3种方法(推荐)
Sep 24 Javascript
js图片轮播手动切换特效
Jan 12 Javascript
详解vue2 $watch要注意的问题
Sep 08 Javascript
js Element Traversal规范中的元素遍历方法
Apr 19 Javascript
小程序兼容安卓和IOS数据处理问题及坑
Sep 18 Javascript
vue模式history下在iis中配置流程
Apr 17 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
自制汽车收音机天线:收听广播的技巧和方法
2021/03/02 无线电
1.PHP简介
2006/10/09 PHP
PHP实现获取并生成数据库字典的方法
2016/05/04 PHP
微信封装的调用微信签名包的类库
2017/06/08 PHP
PHP的mysqli_sqlstate()函数讲解
2019/01/23 PHP
JavaScript 学习初步 入门教程
2010/03/25 Javascript
纯JavaScript实现的完美渐变弹出层效果代码
2010/04/02 Javascript
jQuery基础框架浅入剖析
2012/12/27 Javascript
JS预览图像将本地图片显示到浏览器上
2013/08/25 Javascript
js控制表单不能输入空格的小例子
2013/11/20 Javascript
使用firebug进行调试javascript的示例
2013/12/16 Javascript
js生成验证码并直接在前端判断
2015/05/15 Javascript
Node.js文件操作方法汇总
2016/03/22 Javascript
原生JS和jQuery版实现文件上传功能
2016/04/18 Javascript
JavaScript模仿Pinterest实现图片预加载功能
2016/10/25 Javascript
less简单入门(CSS 预处理语言)
2017/03/08 Javascript
Angular HMR(热模块替换)功能实现方法
2018/04/04 Javascript
微信小程序支付PHP代码
2018/08/23 Javascript
vue语法自动转typescript(解放双手)
2019/09/18 Javascript
浅析JavaScript 函数柯里化
2020/09/08 Javascript
[38:44]DOTA2上海特级锦标赛A组小组赛#2 Secret VS CDEC第二局
2016/02/25 DOTA
跟老齐学Python之玩转字符串(3)
2014/09/14 Python
使用Python脚本操作MongoDB的教程
2015/04/16 Python
简单介绍Python中用于求最小值的min()方法
2015/05/15 Python
Python对列表排序的方法实例分析
2015/05/16 Python
Python输入二维数组方法
2018/04/13 Python
Python 共享变量加锁、释放详解
2019/08/28 Python
Python Django路径配置实现过程解析
2020/11/05 Python
创业计划书的写作技巧及要点
2014/01/31 职场文书
《在家里》教后反思
2014/03/01 职场文书
文明倡议书范文
2014/04/15 职场文书
公证书格式
2015/01/23 职场文书
2015年前台个人工作总结
2015/04/03 职场文书
先进基层党组织主要事迹材料
2015/11/03 职场文书
如何在CSS中绘制曲线图形及展示动画
2021/05/24 HTML / CSS
教你怎么用python selenium实现自动化测试
2021/05/27 Python