JavaScript实例:mini选择器实例代码详解
在网上发现一个JavaScript小型选择器—mini,其介绍在这里已经说得挺清楚了,就不再罗嗦了。简单来说,mini选择器只支持以下选择语句:
* `tag`
* `tag > .className`
* `tag > tag`
* `#id > tag.className`
* `.className tag`
* `tag, tag, #id`
* `tag#id.className`
* `.className`
* `span > * > b`
经过调查,以上选择语句已经满足了95%以上的需求。
mini选择器实例代码如下:
1.
var
pAnchors = mini(
'p > a'
);
// Returns an array.
2.
for
(
var
i = 0, l = pAnchors.length; i < l; ++i) {
3.
// Do stuff...
4.
}
下载源码查看,发现源码并不难,至少比jquery简单得多,就想试着分析一下它的源码,练练手,之前我是想分析jquery源码的,但发现实在太难了,超出能力范围了,还是先从简单的代码开始吧。
mini选择器大体上,就是先把选择语句最右边的元素先选出来,再根据左边的父元素层层过滤得到符合整个语句的元素。
例如”#a table .red”这个语句的选择过程,就是先选出页面上所有class=”red”的dom元素,再在选出来的元素中判断其父元素是否为table,是则保存,不是则丢弃。这层筛选完后,把结果再进行一次筛选,判断其父元素是否id=”a”,是则保留,不是则丢弃,最后就筛选出了符合”#a table .red”的所有dom元素。
其余细节的解析,我用注释的方式加在代码上了。我发现要把分析代码的过程写出来真是很难,代码是看得懂,但就是表达不出来代码的意思。我现在写出来的那些注释,似乎有点乱,估计别人也挺难看懂,不过当练兵吧,我在写之前并没有完全了解mini的原理,写完后就清晰了,看跟写还是有很大区别的,写出来对自己挺有帮助。
有些地方其实我也不是知道得很清晰,可能会有错误存在。代码里我还有一些细节不理解,有疑问的地方我打上了**号,希望高手看到能告知吧~
在这里可以看到,单独选择一个id占了所有选择语句的一半以上,个人感觉mini没有对id进行单独优化,算是不足吧,并且就算只选择一个id,mini(”#id”)返回的也是一个数组,很不方便,实用性不强。
源码解析:
001.
//首先建立一个立刻执行的匿名函数,创建了一个闭包环境(function(){})(),所有代码写在里面,相当于开辟一个私有领域,在里面定义的变量不会影响到全局其他变量。
002.
//此匿名函数最后返回_find(),传给全局变量mini,这样就可以通过mini(selector, context)调用闭包里的_find()进行查询了。_find()是闭包里唯一暴露给外部的函数,其他变量与函数都是私有的,在外部不可见,只能在内部调用。
003.
004.
var
mini = (
function
(){
005.
006.
var
snack = /(?:[\w\-\\.
#]+)+(?:\[\w+?=([\'"])?(?:\\\1|.)+?\1\])?|\*|>/ig,
007.
exprClassName = /^(?:[\w\-_]+)?\.([\w\-_]+)/,
008.
exprId = /^(?:[\w\-_]+)?
#([\w\-_]+)/,
009.
exprNodeName = /^([\w\*\-_]+)/,
010.
//辅助数组,是为了能像这样方便写代码:(part.match(exprClassName) || na)[1]
011.
na = [
null
,
null
];
012.
013.
function
_find(selector, context) {
014.
015.
//没有传入context的话 就默认为document
016.
context = context || document;
017.
018.
//判断是不是只是选择id。这里看起来,只是选择id的话不能使用querySelectorAll?
019.
var
simple = /^[\w\-_
#]+$/.test(selector);
020.
021.
if
(!simple && context.querySelectorAll) {
022.
//如果DOM元素的querySelectorAll方法存在,立即用此方法查找DOM节点,并将结果转换为Array返回。
023.
//querySelectorAll是w3c制定的查询dom标准接口,目前四大个浏览器(firefox3.1 opera10, IE 8, safari 3.1+)都已经支持这个方法,使用浏览器原生支持的方法无疑可以很大地提高查询效率。
024.
return
realArray(context.querySelectorAll(selector));
025.
}
026.
027.
//如果querySelectorAll不存在,就要开始折腾了。
028.
//首先如果查询语句包含了逗号,就把用逗号分开的各段查询分离,调用本身_find查找各分段的结果,显然此时传入_find的查询字符串已经不包含逗号了
029.
//各分段查询结果用concat连接起来,返回时使用下面定义的unique函数确保没有重复DOM元素存在数组里。
030.
if
(selector.indexOf(
','
) > -1) {
031.
var
split = selector.split(/,/g), ret = [], sIndex = 0, len = split.length;
032.
for
(; sIndex < len; ++sIndex) {
033.
ret = ret.concat( _find(split[sIndex], context) );
034.
}
035.
return
unique(ret);
036.
}
037.
038.
//如果不包含逗号,开始正式查询dom元素
039.
//此句把查询语句各个部分分离出来。snack正则表达式看不太懂,大致上就是把"#id div > p"变成数组["#s2", "b", ">", "p"],空格和">"作为分隔符
040.
var
parts = selector.match(snack),
041.
042.
//取出数组里最后一个元素进行分析,由于mini库支持的查询方式有限,能确保在后面的片段一定是前面片段的子元素,例如"#a div",div就是#a的子元素 "#a > p" p是#a的直接子元素
043.
//先把匹配最后一个查询片段的dom元素找出来,再进行父类过滤,就能找出满足整句查询语句的dom元素
044.
part = parts.pop(),
045.
046.
//如果此片段符合正则表达式exprId,那就是一个ID,例如"#header",如果是一个ID,则把ID名返回给变量id,否则返回null
047.
id = (part.match(exprId) || na)[1],
048.
049.
//此句使用a = b && c 的方式,如果b为真,则返回c值赋给a;如果b为假,则直接返回b值给a。(null undefined false 0 "" 等均为假)
050.
//在这个框架里很多这样的用法。如果已经确定此片段类型是ID,就不必执行正则表达式测试它是不是class类型或者node类型了。直接返回null。
051.
//否则就测试它是不是class类型或者node类型,并把名字返回给变量className和nodeName。
052.
className = !id && (part.match(exprClassName) || na)[1],
053.
nodeName = !id && (part.match(exprNodeName) || na)[1],
054.
055.
//collection是用来记录查询结果的
056.
collection;
057.
058.
//如果此片段是class类型,如".red",并且DOM的getElementsByClassName存在(目前Firefox3和Safari支持),直接用此方法查询元素返回给collection
059.
if
(className && !nodeName && context.getElementsByClassName) {
060.
061.
collection = realArray(context.getElementsByClassName(className));
062.
063.
}
else
{
064.
//**不明白这里为什么先查询nodeName再查询className再查询id,个人感觉把id提到前面来不是更能提高效率?
065.
//如果此片段是node类型,则通过getElementsByTagName(nodeName)返回相应的元素给collection。
066.
//如果此片段不是id和node,就会执行collection = realArray(context.getElementsByTagName('*')),返回页面所有元素给collection,为筛选className做准备。
067.
collection = !id && realArray(context.getElementsByTagName(nodeName ||
'*'
));
068.
069.
//如果此片段是class类型,经过上面的步骤collection就储存了页面所有元素,把它传进下面定义的filterByAttr函数,找出符合class="className"的元素
070.
if
(className) {
071.
collection = filterByAttr(collection,
'className'
, RegExp(
'(^|\\s)'
+ className +
'(\\s|$)'
));
072.
}
073.
074.
//此处查询id,如果是id,就不需要考虑此片段的前面那些查询片段,例如"div #a"只需要直接返回id为a的元素就行了。
075.
//直接通过getElementById把它变成数组返回,如果找不到元素则返回空数组
076.
if
(id) {
077.
var
byId = context.getElementById(id);
078.
return
byId?[byId]:[];
079.
}
080.
}
081.
082.
//parts[0]存在,则表示还有父片段需要过滤,如果parts[0]不存在,则表示查询到此为止,返回查询结果collection就行了
083.
//collection[0]存在表示此子片段查询结果不为空。如果为空,不需要再进行查询,直接返回这个空数组。
084.
//还有父片段需要过滤,查询结果又不为空的话,执行filterParents过滤collection的元素,使之符合整个查询语句,并返回结果。
085.
return
parts[0] && collection[0] ? filterParents(parts, collection) : collection;
086.
087.
}
088.
089.
function
realArray(c) {
090.
091.
/**
092.
* 把元素集合转换成数组
093.
*/
094.
095.
try
{
096.
//数组的slice方法不传参数的话就是一个快速克隆的方法
097.
//通过call让传进来的元素集合调用Array的slice方法,快速把它转换成一个数组并返回。
098.
return
Array.prototype.slice.call(c);
099.
}
catch
(e) {
100.
//如果出错,就用原始方法把元素一个个复制给一个新数组并返回。
101.
//**什么时候会出错?
102.
var
ret = [], i = 0, len = c.length;
103.
for
(; i < len; ++i) {
104.
ret[i] = c[i];
105.
}
106.
return
ret;
107.
}
108.
109.
}
110.
111.
function
filterParents(selectorParts, collection, direct) {
112.
113.
//继续把最后一个查询片段取出来,跟_find里的part = parts.pop()一样
114.
var
parentSelector = selectorParts.pop();
115.
116.
//记得分离选择语句各个部分时,"#id div > p"会变成数组["#s2", "b", ">", "p"],">"符号也包含在内。
117.
//如果此时parentSelector是">",表示要查找的是直接父元素,继续调用filterParents,并把表示是否只查找直接父元素的标志direct设为true。
118.
if
(parentSelector ===
'>'
) {
119.
return
filterParents(selectorParts, collection,
true
);
120.
}
121.
122.
//ret存储查询结果 跟_find()里的collection一样 r为ret的数组索引
123.
var
ret = [],
124.
r = -1,
125.
126.
//与_find()里的定义完全一样
127.
id = (parentSelector.match(exprId) || na)[1],
128.
className = !id && (parentSelector.match(exprClassName) || na)[1],
129.
nodeName = !id && (parentSelector.match(exprNodeName) || na)[1],
130.
131.
//collection的数组索引
132.
cIndex = -1,
133.
node, parent,
134.
matches;
135.
136.
//如果nodeName存在,把它转成小写字母以便比较
137.
nodeName = nodeName && nodeName.toLowerCase();
138.
139.
//遍历collection每一个元素进行检查
140.
while
( (node = collection[++cIndex]) ) {
141.
//parent指向此元素的父节点
142.
parent = node.parentNode;
143.
144.
do
{
145.
146.
//如果当前片段是node类型,nodeName是*的话无论如何都符合条件,否则应该让collection里元素的父元素的node名与之相等才符合条件
147.
matches = !nodeName || nodeName ===
'*'
|| nodeName === parent.nodeName.toLowerCase();
148.
//如果当前片段是id类型,就应该让collection里元素的父元素id与之相等才符合条件
149.
matches = matches && (!id || parent.id === id);
150.
//如果当前片段是class类型,就应该让collection里元素的父元素的className与之相等才符合条件
151.
//parent.className有可能前后包含有空格,所以用正则表达式匹配
152.
matches = matches && (!className || RegExp(
'(^|\\s)'
+ className +
'(\\s|$)'
).test(parent.className));
153.
154.
//如果direct=true 也就是说后面的符号是>,只需要查找直接父元素就行了,循环一次立刻break
155.
//另外如果找到了匹配元素,也跳出循环
156.
if
(direct || matches) {
break
; }
157.
158.
}
while
( (parent = parent.parentNode) );
159.
//如果一直筛选不到,则一直循环直到根节点 parent=false跳出循环,此时matches=false
160.
161.
//经过上面的检查,如果matches=true则表示此collection元素符合条件,添加到结果数组里。
162.
if
(matches) {
163.
ret[++r] = node;
164.
}
165.
}
166.
167.
//跟_find()一样,此时collection变成了ret,如果还有父片段,继续进行过滤,否则返回结果
168.
return
selectorParts[0] && ret[0] ? filterParents(selectorParts, ret) : ret;
169.
170.
}
171.
172.
var
unique = (
function
(){
173.
//+new Date()返回时间戳作为唯一标识符
174.
//为了保存变量uid和方法data,使用了一个闭包环境
175.
var
uid = +
new
Date();
176.
177.
var
data = (
function
(){
178.
//为了保存变量n,使用了一个闭包环境
179.
var
n = 1;
180.
181.
return
function
(elem) {
182.
183.
//如果elem是第一次进来检验,cacheIndex=elem[uid]=false,赋给elem[uid]一个值并返回true
184.
//下次再进来检验时elem[uid]有了值,cacheIndex!=flase 就返回false
185.
//**此处不明白nextCacheIndex的作用,随便给elem[uid]一个值不就行了吗
186.
var
cacheIndex = elem[uid],
187.
nextCacheIndex = n++;
188.
189.
if
(!cacheIndex) {
190.
elem[uid] = nextCacheIndex;
191.
return
true
;
192.
}
193.
194.
return
false
;
195.
196.
};
197.
198.
})();
199.
200.
return
function
(arr) {
201.
202.
var
length = arr.length,
203.
ret = [],
204.
r = -1,
205.
i = 0,
206.
item;
207.
208.
//遍历每个元素传进data()增加标志,判断是否有重复元素,重复了就跳过,不重复就赋给ret数组
209.
for
(; i < length; ++i) {
210.
item = arr[i];
211.
if
(data(item)) {
212.
ret[++r] = item;
213.
}
214.
}
215.
216.
//下次调用unique()时必须使用不同的uid
217.
uid += 1;
218.
219.
//返回确保不会有重复元素的数组ret
220.
return
ret;
221.
222.
};
223.
224.
})();
225.
226.
function
filterByAttr(collection, attr, regex) {
227.
228.
/**
229.
* 通过属性名筛选元素
230.
*/
231.
232.
var
i = -1, node, r = -1, ret = [];
233.
//遍历collection里每一个元素
234.
while
( (node = collection[++i]) ) {
235.
//整个框架调用filterByAttr的只有这一句:collection = filterByAttr(collection, 'className', RegExp('(^|\\s)' + className + '(\\s|$)'));
236.
//筛选元素的className,如果符合,加进数组ret,否则跳过
237.
if
(regex.test(node[attr])) {
238.
ret[++r] = node;
239.
}
240.
}
241.
//返回筛选结果
242.
return
ret;
243.
}
244.
245.
//返回_find,暴露给外部的唯一接口
246.
return
_find;
247.
248.
})();