技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> JavaScript --> js selector设计及实现(一)――实现思路

js selector设计及实现(一)――实现思路

浏览:1966次  出处信息
前言

    前阵子和伟大的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 element = document.querySelector(selectors);

    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规则,伪正则描述。

引用
(关系符{1}(标签元素{1})((?:属性选择符)*)(:伪类)?)+
细心些的人应该会提出这样的问题,如果给出这样的selector:document.querySelectorAll(".link") 应该怎么理解?

    ――这代表着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解析优化。有个简单印象之后再随之实践:

引用
假设selector传入为:Dom.querySelectorAll("div.panel div[className='shadow']");
假设HTML结构为:

    

    

     

要找到这个节点

    

    

     

a

     

b

     

c

    

    

    

    我们来写一下从左到右的顺序解析与查找过程:

    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.1 用正则表达式进行解析,存储成attris = [[属性名,运算符,表达式]]。

    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 将上面的过程合成一个新函数,使之可以进行过滤。

    代码如下:

    解析流程图

    

http://www.never-online.net/blog/uploads/201007/selector-parser-workflow.png

建议继续学习:

  1. js selector设计及实现(二)――完善及优化    (阅读:1384)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习
© 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

京ICP备15002552号-1