浅谈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代码
Jul 19 Javascript
js将字符串转成正则表达式的实现方法
Nov 13 Javascript
js实现从中间开始往上下展开网页窗口的方法
Mar 02 Javascript
jquery实现点击弹出可放大居中及关闭的对话框(附demo源码下载)
May 10 Javascript
JS从数组中随机取出几个数组元素的方法
Aug 02 Javascript
jQuery插件zTree实现获取当前选中节点在同级节点中序号的方法
Mar 08 Javascript
jQuery扩展_动力节点Java学院整理
Jul 05 jQuery
无限循环轮播图之运动框架(原生JS实现)
Oct 01 Javascript
微信小程序ajax实现请求服务器数据及模版遍历数据功能示例
Dec 15 Javascript
微信小程序实现上传图片功能
May 28 Javascript
JavaScript实现封闭区域布尔运算的示例代码
Jun 25 Javascript
Vue press 支持图片放大功能的实例代码
Nov 09 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
基于xcache的配置与使用详解
2013/06/18 PHP
Laravel 5 框架入门(三)
2015/04/09 PHP
PHP中array_keys和array_unique函数源码的分析
2016/02/26 PHP
php正确输出json数据的实例讲解
2018/08/21 PHP
用js得到网页中所有的div的id
2020/10/19 Javascript
IE不出现Flash激活框的小发现的js实现方法
2007/09/07 Javascript
风吟的小型JavaScirpt库 (FY.JS).
2010/03/09 Javascript
javascript常用方法、属性集合及NodeList 和 HTMLCollection 的浏览器差异
2010/12/25 Javascript
JS常用函数使用指南
2014/11/23 Javascript
基于Arcgis for javascript实现百度地图ABCD marker的效果
2015/09/12 Javascript
果断收藏9个Javascript代码高亮脚本
2016/01/06 Javascript
JS使用单链表统计英语单词出现次数
2016/06/16 Javascript
jQuery用户头像裁剪插件cropbox.js使用详解
2017/06/07 jQuery
微信小程序实现给嵌套template模板传递数据的方式总结
2017/12/18 Javascript
JavaScript DOM元素常见操作详解【添加、删除、修改等】
2018/05/09 Javascript
Angularjs之如何在跨域请求中传输Cookie的方法
2018/06/01 Javascript
mpvue跳转页面及注意事项
2018/08/03 Javascript
Node.js的进程管理的深入理解
2019/01/09 Javascript
详解vue路由篇(动态路由、路由嵌套)
2019/01/27 Javascript
对layui中table组件工具栏的使用详解
2019/09/19 Javascript
Python time模块详解(常用函数实例讲解,非常好)
2014/04/24 Python
Python fileinput模块使用实例
2015/05/28 Python
matplotlib绘图实例演示标记路径
2018/01/23 Python
对Tensorflow中的变量初始化函数详解
2018/07/27 Python
Python3模拟curl发送post请求操作示例
2019/05/03 Python
python在新的图片窗口显示图片(图像)的方法
2019/07/11 Python
tensorflow没有output结点,存储成pb文件的例子
2020/01/04 Python
Python制作简易版小工具之计算天数的实现思路
2020/02/13 Python
python 项目目录结构设置
2020/02/14 Python
keras 使用Lambda 快速新建层 添加多个参数操作
2020/06/10 Python
python写文件时覆盖原来的实例方法
2020/07/22 Python
Selenium之模拟登录铁路12306的示例代码
2020/07/31 Python
责任担保书范文
2014/05/21 职场文书
新文化运动的口号
2014/06/21 职场文书
jQuery实现影院选座订座效果
2021/04/13 jQuery
SpringBoot前端后端分离之Nginx服务器下载安装过程
2022/08/14 Servers