JavaScript的又一个选择器mini的介绍及源码

在JavaScript中,我们知道封装之后的JQuery的选择器是很强大的一个功能之一,改变了原有的选择DOM节点的方式,如果浏览过JQuery源码的朋友可能会发现在选择器的模块完全是一脸懵逼的状态,选择器基于的Sizzle核心代码,解读起来更是有很大的难度,而目前的市场上也有众多的选择器,包括 Peppy、Sizzle以及 Sly等等,它们都实现了所有的 CSS3 选择器并且性能都非常的优秀。


今天我们要说的是 mini 选择器,首先我们来说说 mini 的作者 James Padolsey 。同时,他也是 jQuery Cookbook 的作者。在推荐 mini 的因素有很多,其中最大的亮点是从实用主义出发,简单高效的完成任务。

简单来说,mini 选择器只支持一下几种选择语句:

  1. div
  2. .example
  3. body div
  4. div, p
  5. div, p, .example
  6. div p
  7. div > p
  8. div.example
  9. ul .example
  10. #title
  11. h1#title
  12. div #title
  13. ul.foo > * span

记得jQuery 的作者 John Resig 曾经统计 jQuery 框架常用的几个选择器。很惊讶的发现用户其实常用的 tagName 、 className 以及 id 就能完成 95% 以上的工作。 而 mini 正式从实际进行出发,是的。虽然不多,但是完全足够我们日常的使用了。

mini 的代码很简单,甚至不用去读懂恐怖的Sizzle这样的每行源代码,mini 的代码示例如下:

  1. var miniDOM = mini("p > a"); // Returns an array.
  2. for (var i = 0, l = miniDOM.length; i < l; ++i) {
  3. // content ... ...
  4. }

mini选择器大体上,就是先把选择语句最右边的元素先选出来,再根据左边的父元素层层过滤得到符合整个语句的元素。

例如”#a table .red”这个语句的选择过程,就是先选出页面上所有class=”red”的dom元素,再在选出来的元素中判断其父元素是否为table,是则保存,不是则丢弃。这层筛选完后,把结果再进行一次筛选,判断其父元素是否id=”a”,是则保留,不是则丢弃,最后就筛选出了符合”#a table .red”的所有dom元素。

其余细节的解析,我用注释的方式加在代码上了。我发现要把分析代码的过程写出来真是很难,代码是看得懂,但就是表达不出来代码的意思。我现在写出来的那些注释,似乎有点乱,估计别人也挺难看懂,不过当练兵吧,我在写之前并没有完全了解mini的原理,写完后就清晰了,看跟写还是有很大区别的,写出来对自己挺有帮助。

有些地方其实我也不是知道得很清晰,可能会有错误存在。代码里我还有一些细节不理解,有疑问的地方我打上了**号,希望高手看到能告知吧~

在这里可以看到,单独选择一个id占了所有选择语句的一半以上,个人感觉mini没有对id进行单独优化,算是不足吧,并且就算只选择一个id,mini(“#id”)返回的也是一个数组,很不方便,实用性不强。

源码解析:

  1. //首先建立一个立刻执行的匿名函数,创建了一个闭包环境(function(){})(),所有代码写在里面,相当于开辟一个私有领域,在里面定义的变量不会影响到全局其他变量。
  2. //此匿名函数最后返回_find(),传给全局变量mini,这样就可以通过mini(selector, context)调用闭包里的_find()进行查询了。_find()是闭包里唯一暴露给外部的函数,其他变量与函数都是私有的,在外部不可见,只能在内部调用。
  3. var mini = (function(data){
  4. var snack = /(?:[w-.#]+)+(?:[w+?=([""])?(?:1|.)+?1])?|*|>/ig,
  5. exprClassName = /^(?:[w-_]+)?.([w-_]+)/,
  6. exprId = /^(?:[w-_]+)?#([w-_]+)/,
  7. exprNodeName = /^([w*-_]+)/,
  8. //辅助数组,是为了能像这样方便写代码:(part.match(exprClassName) || na)[1]
  9. na = [null,null];
  10. function _find(selector, context) {
  11. //没有传入context的话 就默认为document
  12. context = context || document;
  13. //判断是不是只是选择id。这里看起来,只是选择id的话不能使用querySelectorAll?
  14. var simple = /^[w-_#]+$/.test(selector);
  15. if (!simple && context.querySelectorAll) {
  16. //如果DOM元素的querySelectorAll方法存在,立即用此方法查找DOM节点,并将结果转换为Array返回。
  17. //querySelectorAll是w3c制定的查询dom标准接口,目前四大个浏览器(firefox3.1 opera10, IE 8, safari 3.1+)都已经支持这个方法,使用浏览器原生支持的方法无疑可以很大地提高查询效率。
  18. return realArray(context.querySelectorAll(selector));
  19. }
  20. //如果querySelectorAll不存在,就要开始折腾了。
  21. //首先如果查询语句包含了逗号,就把用逗号分开的各段查询分离,调用本身_find查找各分段的结果,显然此时传入_find的查询字符串已经不包含逗号了
  22. //各分段查询结果用concat连接起来,返回时使用下面定义的unique函数确保没有重复DOM元素存在数组里。
  23. if (selector.indexOf(",") > -1) {
  24. var split = selector.split(/,/g), ret = [], sIndex = 0, len = split.length;
  25. for(; sIndex < len; ++sIndex) {
  26. ret = ret.concat( _find(split[sIndex], context) );
  27. }
  28. return unique(ret);
  29. }
  30. //如果不包含逗号,开始正式查询dom元素
  31. //此句把查询语句各个部分分离出来。snack正则表达式看不太懂,大致上就是把"#id div > p"变成数组["#s2", "b", ">", "p"],空格和">"作为分隔符
  32. var parts = selector.match(snack),
  33. //取出数组里最后一个元素进行分析,由于mini库支持的查询方式有限,能确保在后面的片段一定是前面片段的子元素,例如"#a div",div就是#a的子元素 "#a > p" p是#a的直接子元素
  34. //先把匹配最后一个查询片段的dom元素找出来,再进行父类过滤,就能找出满足整句查询语句的dom元素
  35. part = parts.pop(),
  36. //如果此片段符合正则表达式exprId,那就是一个ID,例如"#header",如果是一个ID,则把ID名返回给变量id,否则返回null
  37. id = (part.match(exprId) || na)[1],
  38. //此句使用a = b && c 的方式,如果b为真,则返回c值赋给a;如果b为假,则直接返回b值给a。(null undefined false 0 "" 等均为假)
  39. //在这个框架里很多这样的用法。如果已经确定此片段类型是ID,就不必执行正则表达式测试它是不是class类型或者node类型了。直接返回null。
  40. //否则就测试它是不是class类型或者node类型,并把名字返回给变量className和nodeName。
  41. className = !id && (part.match(exprClassName) || na)[1],
  42. nodeName = !id && (part.match(exprNodeName) || na)[1],
  43. //collection是用来记录查询结果的
  44. collection;
  45. //如果此片段是class类型,如".red",并且DOM的getElementsByClassName存在(目前Firefox3和Safari支持),直接用此方法查询元素返回给collection
  46. if (className && !nodeName && context.getElementsByClassName) {
  47. collection = realArray(context.getElementsByClassName(className));
  48. } else {
  49. //**不明白这里为什么先查询nodeName再查询className再查询id,个人感觉把id提到前面来不是更能提高效率?
  50. //如果此片段是node类型,则通过getElementsByTagName(nodeName)返回相应的元素给collection。
  51. //如果此片段不是id和node,就会执行collection = realArray(context.getElementsByTagName("*")),返回页面所有元素给collection,为筛选className做准备。
  52. collection = !id && realArray(context.getElementsByTagName(nodeName || "*"));
  53. //如果此片段是class类型,经过上面的步骤collection就储存了页面所有元素,把它传进下面定义的filterByAttr函数,找出符合class="className"的元素
  54. if (className) {
  55. collection = filterByAttr(collection, "className", RegExp("(^|s)" + className + "(s|$)"));
  56. }
  57. //此处查询id,如果是id,就不需要考虑此片段的前面那些查询片段,例如"div #a"只需要直接返回id为a的元素就行了。
  58. //直接通过getElementById把它变成数组返回,如果找不到元素则返回空数组
  59. if (id) {
  60. var byId = context.getElementById(id);
  61. return byId?[byId]:[];
  62. }
  63. }
  64. //parts[0]存在,则表示还有父片段需要过滤,如果parts[0]不存在,则表示查询到此为止,返回查询结果collection就行了
  65. //collection[0]存在表示此子片段查询结果不为空。如果为空,不需要再进行查询,直接返回这个空数组。
  66. //还有父片段需要过滤,查询结果又不为空的话,执行filterParents过滤collection的元素,使之符合整个查询语句,并返回结果。
  67. return parts[0] && collection[0] ? filterParents(parts, collection) : collection;
  68. }
  69. function realArray(c) {
  70. /**
  71. * 把元素集合转换成数组
  72. */
  73. try {
  74. //数组的slice方法不传参数的话就是一个快速克隆的方法
  75. //通过call让传进来的元素集合调用Array的slice方法,快速把它转换成一个数组并返回。
  76. return Array.prototype.slice.call(c);
  77. } catch(e) {
  78. //如果出错,就用原始方法把元素一个个复制给一个新数组并返回。
  79. //**什么时候会出错?
  80. var ret = [], i = 0, len = c.length;
  81. for (; i < len; ++i) {
  82. ret[i] = c[i];
  83. }
  84. return ret;
  85. }
  86. }
  87. function filterParents(selectorParts, collection, direct) {
  88. //继续把最后一个查询片段取出来,跟_find里的part = parts.pop()一样
  89. var parentSelector = selectorParts.pop();
  90. //记得分离选择语句各个部分时,"#id div > p"会变成数组["#s2", "b", ">", "p"],">"符号也包含在内。
  91. //如果此时parentSelector是">",表示要查找的是直接父元素,继续调用filterParents,并把表示是否只查找直接父元素的标志direct设为true。
  92. if (parentSelector === ">") {
  93. return filterParents(selectorParts, collection, true);
  94. }
  95. //ret存储查询结果 跟_find()里的collection一样 r为ret的数组索引
  96. var ret = [],
  97. r = -1,
  98. //与_find()里的定义完全一样
  99. id = (parentSelector.match(exprId) || na)[1],
  100. className = !id && (parentSelector.match(exprClassName) || na)[1],
  101. nodeName = !id && (parentSelector.match(exprNodeName) || na)[1],
  102. //collection的数组索引
  103. cIndex = -1,
  104. node, parent,
  105. matches;
  106. //如果nodeName存在,把它转成小写字母以便比较
  107. nodeName = nodeName && nodeName.toLowerCase();
  108. //遍历collection每一个元素进行检查
  109. while ( (node = collection[++cIndex]) ) {
  110. //parent指向此元素的父节点
  111. parent = node.parentNode;
  112. do {
  113. //如果当前片段是node类型,nodeName是*的话无论如何都符合条件,否则应该让collection里元素的父元素的node名与之相等才符合条件
  114. matches = !nodeName || nodeName === "*" || nodeName === parent.nodeName.toLowerCase();
  115. //如果当前片段是id类型,就应该让collection里元素的父元素id与之相等才符合条件
  116. matches = matches && (!id || parent.id === id);
  117. //如果当前片段是class类型,就应该让collection里元素的父元素的className与之相等才符合条件
  118. //parent.className有可能前后包含有空格,所以用正则表达式匹配
  119. matches = matches && (!className || RegExp("(^|s)" + className + "(s|$)").test(parent.className));
  120. //如果direct=true 也就是说后面的符号是>,只需要查找直接父元素就行了,循环一次立刻break
  121. //另外如果找到了匹配元素,也跳出循环
  122. if (direct || matches) { break; }
  123. } while ( (parent = parent.parentNode) );
  124. //如果一直筛选不到,则一直循环直到根节点 parent=false跳出循环,此时matches=false
  125. //经过上面的检查,如果matches=true则表示此collection元素符合条件,添加到结果数组里。
  126. if (matches) {
  127. ret[++r] = node;
  128. }
  129. }
  130. //跟_find()一样,此时collection变成了ret,如果还有父片段,继续进行过滤,否则返回结果
  131. return selectorParts[0] && ret[0] ? filterParents(selectorParts, ret) : ret;
  132. }
  133. var unique = (function(){
  134. //+new Date()返回时间戳作为唯一标识符
  135. //为了保存变量uid和方法data,使用了一个闭包环境
  136. var uid = +new Date();
  137. var data = (function(){
  138. //为了保存变量n,使用了一个闭包环境
  139. var n = 1;
  140. return function(elem) {
  141. //如果elem是第一次进来检验,cacheIndex=elem[uid]=false,赋给elem[uid]一个值并返回true
  142. //下次再进来检验时elem[uid]有了值,cacheIndex!=flase 就返回false
  143. //**此处不明白nextCacheIndex的作用,随便给elem[uid]一个值不就行了吗
  144. var cacheIndex = elem[uid],
  145. nextCacheIndex = n++;
  146. if(!cacheIndex) {
  147. elem[uid] = nextCacheIndex;
  148. return true;
  149. }
  150. return false;
  151. };
  152. })();
  153. return function(arr) {
  154. var length = arr.length,
  155. ret = [],
  156. r = -1,
  157. i = 0,
  158. item;
  159. //遍历每个元素传进data()增加标志,判断是否有重复元素,重复了就跳过,不重复就赋给ret数组
  160. for (; i < length; ++i) {
  161. item = arr[i];
  162. if (data(item)) {
  163. ret[++r] = item;
  164. }
  165. }
  166. //下次调用unique()时必须使用不同的uid
  167. uid += 1;
  168. //返回确保不会有重复元素的数组ret
  169. return ret;
  170. };
  171. })();
  172. function filterByAttr(collection, attr, regex) {
  173. /**
  174. * 通过属性名筛选元素
  175. */
  176. var i = -1, node, r = -1, ret = [];
  177. //遍历collection里每一个元素
  178. while ( (node = collection[++i]) ) {
  179. //整个框架调用filterByAttr的只有这一句:collection = filterByAttr(collection, "className", RegExp("(^|s)" + className + "(s|$)"));
  180. //筛选元素的className,如果符合,加进数组ret,否则跳过
  181. if (regex.test(node[attr])) {
  182. ret[++r] = node;
  183. }
  184. }
  185. //返回筛选结果
  186. return ret;
  187. }
  188. //返回_find,暴露给外部的唯一接口
  189. return _find;
  190. })();

没错,是的 mini 的源码就是这么简单易懂。

评论: