前阵子和伟大的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"]
使用示例:
- 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由上面所述四类构成。
以下是描述selector规则,伪正则描述。
(关系符{1}(标签元素{1})((?:属性选择符)*)(:伪类)?)+
细心些的人应该会提出这样的问题,如果给出这样的selector:document.querySelectorAll(“.link”) 应该怎么理解?
——这代表着document根元素下所有className为link的节点集合。可以等价为document.querySelectorAll(” .link”)(注意:.link前有空格)
也就是说,[b]如果传入的selector第一个字符不是关系符,那么我们默认会认为它以空格关系符开始[/b]
解析selector表达式与实现思路
总体思路:由左往右一步步的方式,在查找过程中进行节点滤重。理论实现流程:
有个简单印象之后再随之实践,假设selector传入为:
Fox.query(“div.panel div[className='shadow']“);
假设HTML结构为:
- <div>
-
<div class="panel">
-
<div class="sd">要找到这个节点</div>
-
</div>
-
<div>
-
<div>a</div>
-
<div>b</div>
-
<div>c</div>
-
</div>
-
</div>
我们来写一下从左到右的顺序解析与查找过程:
1. 快捷方式转换。
暂且称为parseShortcut函数吧,
将”div.panel div[className='shadow']“转换成”div[className~='panel' div[className='shadow']“
这部分的代码相对简单:
- 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++) {
-
selectorselector = selector.replace(shortcut[i][0], shortcut[i][1]);
- }
- return selector;
- }
-
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这个数组去解析表达式即可。
代码如下:
- function selectorParser(selector) {
-
var regExp = /(^|\s*[>+~ ]\s*)(([\w\-\:.#*]+|\([^\)]*\)|\[[^\]]*\])+)(?=($|\s*[>+~ ]\s*))/g;
-
var selectors = [];
-
selectorselector = 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'));
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 将上面的过程合成一个新函数,使之可以进行过滤。
代码如下:
- /**
- 单独属性过滤
- */
- 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+ '")';
- };
-
- //属性的格式是[[名,运算符,值]]
-
selectorselector = 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"]'));
解析流程图
以下流程先不考虑selector里有“,”号的情况,例如Fox.query(“div,span”)。为了简单看流程,只说明没有“,”号的情况的实现流程。(注:有”,”号的情况是需要求并集,再对DOM节点排序的)

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