且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

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

更新时间:2022-09-28 15:19:24

前阵子和伟大的JK同学学习了一下目前我们框架里新版本的selector,这里列的是第一版selector的代码思路。
后一版本调优性能,多了些函数,从性能上与各大框架比还是有竞争力的。

说句实在话,虽然各大框架和库都实现了selector。但看他们的selector实现其他的人看上去无疑都是难看懂。
而google,baidu上query出的结果基本都是说使用方式的文章,基本没有类似针对selector设计和具体实现上的文章。
所以,决定将整个思路和实现写出来,一来是增加印象,二来是给目前想写的人以参考。

我是以我学习及写selector的角度及把我向JK学习思路和我自己的设计,代码写的思路写出来。
这篇文章我也想不到写了这么长。建议这么看比较好:

  • 不熟悉selector用户先去熟悉了休息会,再看此文;文中没有写详细的selector的具体内容,只是为了描述,大略的提了下;
  • selector了解了之后再看看思路;顺序解析还是比较容易看懂的;
  • 后文中的js代码里,我做了详细的注释,结构也和文中提的代码结构一样,有兴趣的同学可以读下,这个selector代码暂名为:Fox,接口为Fox.query(selector, context)。

代码可以点这里下载和测试
此份代码已经注释,比较简单,我没去写优化的代码,之所以代码里面不写,是因为写了解释起来很绕,熟悉思路先吧,以后详细优化的文章及代码可以抽空再写。

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");

具体在代码里的表现形式

Fox.query(selector, context);

selector及其类型

selector是一种选择DOM元素/集合的一种符号。它包括以下的类型:

  • 包括通配选择符——*
  • 类型选择符——如E { sRules }
  • 属性选择符——它包含四种等式:
    • E[attr] 选择具有 attr 属性的 E
    • E[attr=value] 选择具有 attr 属性且属性值等于 value 的 E
    • E[attr~=value] 选择具有 attr 属性且属性值为一用空格分隔的字词列表,其中一个等于 value 的 E 。这里的 value 不能包含空格
    • E[attr|=value] 选择具有 attr 属性且属性值为一用连字符分隔的字词列表,由 value 开始的 E
    • E[attr^=value] 选择具有 attr 属性开始的值为value的 E
    • E[attr$=value] 选择具有 attr 属性结尾的值为value的 E
    • E[attr*=value] 选择具有 attr 属性里包含value的E
  • 包含选择符(祖先)——如E1 E2 选择所有被 E1 包含的 E2 。即 E1.contains(E2)==true 。
  • 子对象选择符——如E1 > E2 选择所有作为 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表

*
E
E F
E > F
E + F
E ~ F
E.warning
E#myid
E:first-child
E:last-child
E:nth-child(n)
E:nth-last-child(n)
E:only-child
E:enabled
E:disabled
E:checked
E:contains(“foo”)
E:not(s)
E[foo]
E[foo="bar"]
E[foo~="bar"]
E[foo^="bar"]
E[foo$="bar"]
E[foo*="bar"]
E[foo|="bar"]

使用示例:


  1. alert(Fox.query('div~div', document.body).length);  
  2. alert(Fox.query('div~div.aa', document.body).length);  
  3. alert(Fox.query('div span', document.body).length);  
  4. alert(Fox.query('div div', document.body).length);  
  5. alert(Fox.query('div>input[type="text"]', document.body).length);  
  6. alert(Fox.query('input[type="text"]', document.body).length);  
  7. alert(Fox.query('*[type="text"]', document.body).length);  
  8. (function nthTest() {  
  9. var arr = Fox.query('tr:nth-child(2n)');  
  10. for (var i=0; i<arr.length; i++) {  
  11.     arr[i].style.background = "#eee";  
  12. }  
  13. })();  

总结归纳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前有空格)
也就是说,[b]如果传入的selector第一个字符不是关系符,那么我们默认会认为它以空格关系符开始[/b]

解析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传入为:

Fox.query(“div.panel div[className='shadow']“);

假设HTML结构为:


  1. <div> 
  2. <div class="panel"> 
  3. <div class="sd">要找到这个节点</div> 
  4. </div> 
  5. <div> 
  6. <div>a</div> 
  7. <div>b</div> 
  8. <div>c</div> 
  9. </div> 
  10. </div> 

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

1. 快捷方式转换。

暂且称为parseShortcut函数吧,
将”div.panel div[className='shadow']“转换成”div[className~='panel' div[className='shadow']“
这部分的代码相对简单:


  1. function parseShortcuts(selector) {  
  2.     var shortcut = [  
  3.         [/\#([\w\-]+)/g , '[id="$1"]'],//id缩略写法  
  4.         [/\.([\w\-]+)/g , '[className~="$1"]']//className缩略写法  
  5.     ];  
  6.     for (var i=0len=shortcut.length; i<len; i++) {  
  7.         selectorselector = selector.replace(shortcut[i][0], shortcut[i][1]);  
  8.     }  
  9.     return selector;  
  10. }  
  11. alert('div.panel div[className="shadow"]返回的标准表达式为: ' +parseShortcuts('div.panel div[className="shadow"]'));  

2. 表达式解析第一步

2.1 解析关系符及标签,分离出主要关系与需要过滤的属性,上面的解析成:
selectors=[['','div[className~="panel"]‘],[' ','div[className="shadow"]‘]];
//即selectors=[[relation,filters]];
2.2 随即我们只需要顺序循环selectors这个数组去解析表达式即可。
代码如下:


  1. function selectorParser(selector) {  
  2.     var regExp    = /(^|\s*[>+~ ]\s*)(([\w\-\:.#*]+|\([^\)]*\)|\[[^\]]*\])+)(?=($|\s*[>+~ ]\s*))/g;  
  3.     var selectors = [];  
  4.     selectorselector = selector.replace(regExp, function(all, relation, others) {  
  5.         selectors.push([relation, others]);  
  6.         return ''; //将输入参数进行替代,最后不为空,则输入的selector不合法。  
  7.     });  
  8.     if (!/\s*/.test(selector)) throw new Error(['selector unexpect expression['+selector+']']);  
  9.     return selectors;  
  10. }  
  11. alert("div[className~='panel'] div[className='shadow']第一次解析结果:\n" +selectorParser("div[className~='panel'] div[className='shadow']").join('\n'));  

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

代码如下:



  1. /**  
  2. 单独属性过滤  
  3. */  
  4. function parseToFilter(selector) {  
  5.  
  6.     var attriReg  = /\[\s*([\w\-]+)\s*([!~|^$*]?\=)?\s*(?:(["']?)([^\]'"]*)\3)?\s*\]/g,  
  7.         attris    = [],  
  8.         attriFunctions = [],  
  9.         operators = {  
  10.             '~=' : 'attriHandle && (" "+attriHandle+" ").indexOf(" "+attriValue+" ")>-1',  
  11.             '='  : 'attriHandle && attriHandle==attriValue'  
  12.             /**  
  13.             '^=' : TODO,  
  14.             '$=' : TODO,  
  15.             '*=' : TODO,  
  16.             '!=' : TODO  
  17.             */  
  18.         },  
  19.         attriHandle = function(attri) {  
  20.             /* 是否使用内置.attribute形式来获取属性 */  
  21.  
  22.             //内置attribute相关属性转换  
  23.             var attriMap = {  
  24.                 'class': 'el.className',  
  25.                 'for'  : 'el.htmlFor',  
  26.                 'href' : 'el.getAttribute("href", 2)'  
  27.             };  
  28.  
  29.             //优先.attribute属性获取  
  30.             var nativeAttris = 'name,id,className,value,selected,checked,disabled,type,tagName,readOnly'.split(',');  
  31.  
  32.             //内置属性获取  
  33.             for (var i=0len=nativeAttris.length; i<len; i++) {  
  34.                 attriMap[nativeAttris[i]] = 'el.'+nativeAttris[i];  
  35.             }  
  36.  
  37.             return attriMap[attri] || 'el.getAttribute("' +attri+ '")';  
  38.         };  
  39.  
  40.     //属性的格式是[[名,运算符,值]]  
  41.     selectorselector = selector.replace(attriReg,  
  42.                                 function(a,b,c,d,e) {attris.push([b,c||"",e||""]);return "";});  
  43.  
  44.     for (var i=0; i<attris.length; i++) {  
  45.  
  46.         var getAttri = attriHandle(attris[i][0]);  
  47.         var operator = operators[attris[i][1]];  
  48.         var attriVal = attris[i][2];  
  49.  
  50.         attriFunctions.push(  
  51.         operator.replace(/attriHandle/g, getAttri).replace('attriValue', attriVal)  
  52.         );  
  53.  
  54.     };  
  55.  
  56.     attriFunctions = 'return ' +attriFunctions.join('&&');  
  57.     return new Function("el", attriFunctions);  
  58.  
  59. };  
  60. alert('div[className~="panel"]返回的过滤函数为: ' +parseToFilter('div[className~="panel"]')); 

解析流程图

以下流程先不考虑selector里有“,”号的情况,例如Fox.query(“div,span”)。为了简单看流程,只说明没有“,”号的情况的实现流程。(注:有”,”号的情况是需要求并集,再对DOM节点排序的)

 

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

【本文首发于:百度泛用户体验http://www.baiduux.com/blog/2010/07/21/js-selector-design-and-implement/
关注百度技术沙龙











本文转自百度技术51CTO博客,原文链接:http://blog.51cto.com/baidutech/746875,如需转载请自行联系原作者