js selector设计及实现(一)――实现思路
前阵子和伟大的JK同学学习了一下目前我们框架里新版本的selector,这里列的是第一版selector的代码思路。
后一版本调优性能,多了些函数,从性能上与各大框架的性能比还是有竞争力的。
说句实在话,虽然各大框架和库都实现了selector。但看他们的selector实现其他的人看上去无疑都是难看懂。
而google,baidu上query出的结果基本都是说使用方式的文章,基本没有类似针对selector设计和具体实现上的文章。
所以,决定将整个思路和实现写出来,一来是增加印象,二来是给目前想写的人以参考。
我是以我学习及写selector的角度及把我向JK学习思路和我自己的设计,代码写的思路写出来。
这篇文章我也想不到写了这么长。建议这么看比较好:不熟悉selector用户先去熟悉了休息会,再看此文;文中没有写详细的selector的具体内容,只是为了描述,大略的提了下;selector了解了之后再看看思路;顺序解析还是比较容易看懂的;后文中的js代码里,我做了详细的注释,结构也和文中提的代码结构一样,有兴趣的同学可以读下。我的blog没有代码高亮,所以看代码会有点累。>_<
OK,开始吧。
为什么有selector?
selector原来是用于CSS开发时方便样式与结构分离的策略。
而在如今做JS/DOM开发的时候,绝大部分的代码之一都是选择目标元素/集合。
在XML里有XPATH来实现该功能;同理的,在JS/DOM开发时自然出现了selector。
现在selector的火很大程度上除了需要感谢国家,还要感谢jquery。它如同当年Prototype带来了Ruby风格,一大批的前端开发人员都投入到jquery的怀抱。给了很多前端开发人员以快速上手,插件copy的方式来开发前端程序。
jquery是推进selector使用的催化剂。现在很多浏览器都支持了selector,但各实现都不尽相同,所以做一个适合自己的selector目前来看是有必要的。
selector简单实用,减少无技术含量的工作。
还可以重新约束一下前端的UI框架,在render接口不是耦合HTML结构,而是与CSS selector做为桥接
具体可以点这里可以看我之前写的一篇文章(降低HTML结构与脚本之间的强耦合),这里不再多述。
selector的应用接口
selector提供给外部的接口应该尽量遵循标准。开放的接口应该包括:document|element.querySelector(str)document|element.querySelectorAll(str)举例说明:
var matches = document.querySelectorAll("div.note, div.alert");
具体在代码里的表现形式
Dom.querySelector(strSelector, context);
Dom.querySelectorAll(strSelector, context);
selector及其类型
selector是一种选择DOM元素/集合的一种符号。它包括以下的类型:包括通配选择符――*类型选择符――如E { sRules } 属性选择符――它包含四种等式:E [ attr ] { sRules } 选择具有 attr 属性的 E
E [ attr = value ] { sRules } 选择具有 attr 属性且属性值等于 value 的 E
E [ attr ~= value ] { sRules } 选择具有 attr 属性且属性值为一用空格分隔的字词列表,其中一个等于 value 的 E 。这里的 value 不能包含空格
E [ attr |= value ] { sRules } 选择具有 attr 属性且属性值为一用连字符分隔的字词列表,由 value 开始的 E
包含选择符(祖先)――如E1 E2 { sRules } 选择所有被 E1 包含的 E2 。即 E1.contains(E2)==true 。子对象选择符――如E1 > E2 { sRules } 选择所有作为 E1 子对象的 E2 。 ID选择符――#ID { sRules } 以文档目录树(DOM)中作为对象的唯一标识符的 ID 作为选择符。类选择符――E.className { sRules } ,它是属性选择符的一种简写形式。其效果等同于E [ class ~= className ] 。伪类选择符――E : Pseudo-Classes { sRules } JS selector里取到的伪类有如下几种:"first-child","last-child", "only-child","nth-child","nth-last-child","first-of-type","last-of-type",
"only-of-type","nth-of-type","nth-last-of-type","empty","parent",
"not","enabled","disabled","checked","contains"
伪对象选择符。E : Pseudo-Elements { sRules } 这在JS selector里可不实现(在DOM树里无法找到)总结归纳selector语法
要想写好selector,必然要熟悉selector的语法,功能。这也是重中之重。
观察selector的语法,将所有selector分为四类:标签元素――标签就不解释了,但需要注意的是如果没有标签元素,则为选择符里的通配符。例如这样的selector:"div .link"表示,div后裔节点中所有节点里属性className为link的元素集。
选择符――包含“通配符、类型符、属性符。”(注:属性选择符包括了".link"这样的selector。也包括了"#id"这样的selector。)伪类――例如:last-child,first-child等伪类。关系符――包括:“祖先、儿子、相邻兄弟。”总结,任意一个selector由上面所述四类构成。
以下是描述selector规则,伪正则描述。
――这代表着document根元素下所有className为link的节点集合。可以等价为document.querySelectorAll(" .link")(注意:.link前有空格)
也就是说,如果传入的selector第一个字符不是关系符,那么我们默认会认为它以空格关系符开始
解析selector表达式与实现思路
从接口(需求)上分析,先暂不考虑效率问题的情况下,去考虑理论实现流程:1.从入口的参数进行解析,即document.querySelectorAll("div.note, div.alert")参数解析成格式化好的形式方便处理。2.循环解析出来的单个selector,将快捷选择符转换为标准选择符。如上所述,例如将#id属性选择转成[id='id']。3.用getElementsByTagName得到集合,再根据条件进行过滤。4.最后除重。将所有找到的元素集合concat连接,再除重过滤。这里顺便提一下,为什么要除重,例如:document.querySelectorAll("div a","div.alert a"),很明显,"div a"包含"div.alert a",所谓除重就是求各子selector的并集。
6.之后可能会有针对不同的selector作优化或者作特殊处理。――例如nth-child、selector解析优化。有个简单印象之后再随之实践:
我们来写一下从左到右的顺序解析与查找过程:
1. 快捷方式转换。
暂且称为parseShortcut函数吧,
将"div.panel div[className='shadow']"转换成"div[className~='panel' div[className='shadow']"
这部分的代码相对简单:
2. 表达式解析第一步
2.1 解析关系符及标签,分离出主要关系与需要过滤的属性,上面的解析成:
selectors=[['','div[className~="panel"]'],[' ','div[className="shadow"]']];
//即selectors=[[relation,filters]];
2.2 随即我们只需要顺序循环selectors这个数组去解析表达式即可。
代码如下:
3. 分而治之,逐个解析关系
3.1 顺序再解析selectors变量。如第一个元素:['','div[className~="panel"]']
3.2 如上所述的流程,我们会从documentElement开始查找;
3.3 解析第一个元素'',为空,可以先从tagName里开始查找;
3.4 解析出['','div[className~="panel"]']的tagName为div;
3.5 这一步最终会得到document.documentElement.getElementTagName('div');
我们给这个结果命名为divs。
4. 分而治之,过滤得到的集合
因为['','div[className~="panel"]']所含的div节点className必须包含panel,所以我们需要将divs里的节点集合进行过滤才能得到这一级的正确结果。
这么看,我们急需一个过滤属性的函数。这个过滤函数的功能是:
4.1 输入:将div[className~="panel"]表达式传入;
4.2 输出:返回一个新函数function(el){return el.hasClass('panel');}。
注意:其它的attribute也类似,只不过需要做的是有内置属性与自定义属性之分。
4.3 最后看过程:
在返回函数之前我们还需要解析一下[className~="panel"]表达式,以特定格式存储,从而使程序进行处理。将属性选择器归纳起来的语法是:
4.3.2 循环attris
4.3.3 根据属性名得到获取属性的方式,例如属性for在JS里是用htmlFor。而className这类的属性直接用“.”运算符就可以了,不需要用自定义属性的方式el.getAttribute("className")。
4.3.4 根据运算符,得到不同的attribute处理方式。例如~=是'el.className && (" "+el.className+" ").indexOf(" "+attriValue+" ")>-1'。
4.3.5 将上面的过程合成一个新函数,使之可以进行过滤。
代码如下:
解析流程图
建议继续学习:
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:Rank <null@null.com> 来源: never-online
- 标签: selector
- 发布时间:2010-08-12 23:32:21
- [55] IOS安全–浅谈关于IOS加固的几种方法
- [54] Oracle MTS模式下 进程地址与会话信
- [54] 图书馆的世界纪录
- [52] android 开发入门
- [51] 如何拿下简短的域名
- [51] 【社会化设计】自我(self)部分――欢迎区
- [49] Go Reflect 性能
- [47] 读书笔记-壹百度:百度十年千倍的29条法则
- [34] 程序员技术练级攻略
- [32] 文言文白话文互转:文言文转白话文(现代文),