Logo

never-online

A crisis is a terrible thing to waste.
  • Blog首页
  • 推荐日志
  • 关于我
  • 留言簿
  • 设计
  • 订阅RSS
  • 登录
« 写前端文档
js selector设计及实... »
分类: Web Dev
推荐日志

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

[ 2010-07-02 16:56:20 | 作者: Rank ]
字体大小: 大 | 中 | 小
Close Advertisement
see also js selector设计及实现(二)
前言

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

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

我是以我学习及写selector的角度及把我向JK学习思路和我自己的设计,代码写的思路写出来。
这篇文章我也想不到写了这么长。建议这么看比较好:
  • 不熟悉selector用户先去熟悉了休息会,再看此文;文中没有写详细的selector的具体内容,只是为了描述,大略的提了下;
  • selector了解了之后再看看思路;顺序解析还是比较容易看懂的;
  • 后文中的js代码里,我做了详细的注释,结构也和文中提的代码结构一样,有兴趣的同学可以读下,这个selector代码暂名为:Fox,接口为Fox.query(selector, context)。
我的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)
举例说明:
Copy Code(拷贝代码)-Run HTML(运行代码)-Save Code(另存代码)
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"]
使用示例:
Copy Code(拷贝代码)-Run HTML(运行代码)-Save Code(另存代码)
//alert(Fox.query('div~div', document.body).length);
//alert(Fox.query('div~div.aa', document.body).length);
//alert(Fox.query('div span', document.body).length);
//alert(Fox.query('div div', document.body).length);
//alert(Fox.query('div>input[type="text"]', document.body).length);
//alert(Fox.query('input[type="text"]', document.body).length);
//alert(Fox.query('*[type="text"]', document.body).length);
(function nthTest() {
var arr = Fox.query('tr:nth-child(2n)');
for (var i=0; i<arr.length; i++) {
  arr[i].style.background='#eee';
}
})();

总结归纳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传入为:Fox.query("div.panel div[className='shadow']");
假设HTML结构为:
Copy Code(拷贝代码)-Run HTML(运行代码)-Save Code(另存代码)
<body>
<div id="doc">
  <div class="panel">
    <div class="sd">要找到这个节点</div>
  </div>
  <div>
    <div id="a">a</div>
    <div id="b">b</div>
    <div id="c">c</div>
  </div>
</div>
</body>

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

1. 快捷方式转换。

暂且称为parseShortcut函数吧,
将"div.panel div[className='shadow']"转换成"div[className~='panel' div[className='shadow']"
这部分的代码相对简单:
Copy Code(拷贝代码)-Run HTML(运行代码)-Save Code(另存代码)
<script type="text/javascript">//<![CDATA[
function parseShortcuts(selector) {
  var shortcut = [
    [/\#([\w\-]+)/g , '[id="$1"]'],//id缩略写法
    [/\.([\w\-]+)/g , '[className~="$1"]']//className缩略写法
  ];
  for (var i=0, len=shortcut.length; i<len; i++) {
    selector = selector.replace(shortcut[i][0], shortcut[i][1]);
  }
  return selector;
}
alert("div.panel div[className='shadow']返回的标准表达式为: " +parseShortcuts("div.panel div[className='shadow']"));
//]]></script>

2. 表达式解析第一步

2.1 解析关系符及标签,分离出主要关系与需要过滤的属性,上面的解析成:
selectors=[['','div[className~="panel"]'],[' ','div[className="shadow"]']];
//即selectors=[[relation,filters]];
2.2 随即我们只需要顺序循环selectors这个数组去解析表达式即可。
代码如下:
Copy Code(拷贝代码)-Run HTML(运行代码)-Save Code(另存代码)
<script type="text/javascript">//<![CDATA[
function selectorParser(selector) {
  var regExp = /(^|\s*[>+~ ]\s*)(([\w\-\:.#*]+|\([^\)]*\)|\[[^\]]*\])+)(?=($|\s*[>+~ ]\s*))/g;
  var selectors = [];
  selector = selector.replace(regExp, function(all, relation, others) {
    selectors.push([relation, others]);
    return ''; //将输入参数进行替代,最后不为空,则输入的selector不合法。
  });
  if (!/\s*/.test(selector)) throw new Error(['selector unexpect expression['+selector+']']);
  return selectors;
}
alert("div[className~='panel'] div[className='shadow']第一次解析结果:\n" +selectorParser("div[className~='panel'] div[className='shadow']").join('\n'));
</script>

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

代码如下:
Copy Code(拷贝代码)-Run HTML(运行代码)-Save Code(另存代码)
<script type="text/javascript">//<![CDATA[
/**
单独属性过滤
*/
function parseToFilter(selector) {

  var attriReg = /\[\s*([\w\-]+)\s*([!~|^$*]?\=)?\s*(?:(["']?)([^\]'"]*)\3)?\s*\]/g,
    attris = [],
    attriFunctions = [],
    operators = {
      '~=' : 'attriHandle && (" "+attriHandle+" ").indexOf(" "+attriValue+" ")>-1',
      '=' : 'attriHandle && attriHandle==attriValue'
      /**
      '^=' : TODO,
      '$=' : TODO,
      '*=' : TODO,
      '!=' : TODO
      */
    },
    attriHandle = function(attri) {
      /* 是否使用内置.attribute形式来获取属性 */

      //内置attribute相关属性转换
      var attriMap = {
        'class': 'el.className',
        'for' : 'el.htmlFor',
        'href' : 'el.getAttribute("href", 2)'
      };

      //优先.attribute属性获取
      var nativeAttris = 'name,id,className,value,selected,checked,disabled,type,tagName,readOnly'.split(',');

      //内置属性获取
      for (var i=0, len=nativeAttris.length; i<len; i++) {
        attriMap[nativeAttris[i]] = 'el.'+nativeAttris[i];
      }

      return attriMap[attri] || 'el.getAttribute("' +attri+ '")';
    };

  //属性的格式是[[名,运算符,值]]
  selector = selector.replace(attriReg,
                function(a,b,c,d,e) {attris.push([b,c||"",e||""]);return "";});

  for (var i=0; i<attris.length; i++) {

    var getAttri = attriHandle(attris[i][0]);
    var operator = operators[attris[i][1]];
    var attriVal = attris[i][2];

    attriFunctions.push(
    operator.replace(/attriHandle/g, getAttri).replace('attriValue', attriVal)
    );

  };

  attriFunctions = 'return ' +attriFunctions.join('&&');
  return new Function("el", attriFunctions);
  
};
alert('div[className~="panel"]返回的过滤函数为: ' +parseToFilter('div[className~="panel"]'));
//]]></script>

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


see also js selector设计及实现(二)
[最后修改由 Rank, 于 2010-07-12 15:57:20]
评论Feed 评论Feed: http://www.never-online.net/blog/feed.asp?q=comment&id=295

这篇日志没有评论.

发表
表情图标
[smile] [confused] [cool] [cry]
[eek] [angry] [wink] [sweat]
[lol] [stun] [razz] [redface]
[rolleyes] [sad] [yes] [no]
[heart] [star] [music] [idea]
UBB代码
转换链接
表情图标
悄悄话
昵        称:  3-24字符, 不可使用特殊字符 *
安全规则: 请输入规则答案: 2+5=? *
 
Language Package
  • ENGLISH
  • 简体中文
用户面板
用户名:
密码:
安全规则: 2+5=?
注册
分类
  • Blog首页
  • Android [2] Android RSS Feed
  • Diary & Misc [115] Diary & Misc RSS Feed
  • Web Dev [112] Web Dev RSS Feed
  • Never Modules(JS) [12] Never Modules(JS) RSS Feed
  • Flash & Flex & Air [4] Flash & Flex & Air RSS Feed
  • PHP & Apache [1] PHP & Apache RSS Feed
  • XML [7] XML RSS Feed
  • CSS [7] CSS RSS Feed
  • ASP & .NET [3] ASP & .NET RSS Feed
  • Literature Archives [4] Literature Archives RSS Feed
  • Design [17] Design RSS Feed
  • Visual Basic [3] Visual Basic RSS Feed
最新评论
  • 很精彩
  • 好文,收藏至20ju.com
  • 好文,收藏至20ju.com
  • 弹出的匹配框不能随着页...
  • @lily 多年前的东西了,...
  • 为什么在chrome中不支持...
  • @gzman 那阵子确实想蛮多...
  • 兄弟,想太多了吧
  • [smile] [wink] [sweat] ...
  • javascript:insertSmilie...
  • 我新建了两个sliderbar都...
  • 不错哦````````
  • 好文,收藏至20ju.com
  • @aflyhorse 我这里没有实...
  • 我上次找了一个识别效果...
搜索

统计数据
日志: 287
评论: 851
引用: 0
用户: 116
到访: 4132960
在线: 1

新浪微博
Links
  • 阿肆
  • 好奇
  • 小龙人
  • 小萌
  • Zerray
  • realdodo
  • ps album
  • my flickr
  • XiaoFeng
  • 神~ORZ
  • Jiuan's blog
  • yanpeng's blog
  • zhoux's blog
  • winter
  • aoao
  • jerry.qu
  • JoelLeung
  • monyer
  • Miller
  • PuterJam
  • Terry
  • JK
  • akira
  • dh20156's New World!
  • Joshua
  • Estyle
  • 互联网人
  • 兔子
  • 电脑爱好者
  • 阿笨狗
Favorite
  • leica china
  • Douglas Crockford
  • dhteumeuleu
  • regexplib
  • webfx
  • ajaxian
  • John Resig
  • dean
  • Adam McCrea
  • css beauty
  • livepipe
  • smashing magazine
  • ericlippert
  • narcissus
  • PPK
widget

Powered by LBS Version 2.0.304 © 2003-2005 SiC/CYAN. - Template writen by never-online - 桂ICP备07010684号
16 DB Queries | Proccessed in 125ms