当前位置: 首页 > 图文教程 > 网络编程 > Javascript > JavaScript实例:mini选择器实例代码详解

Javascript
jquery ajax提交表单数据的两种方式
js数字输入框(包括最大值最小值限制和四舍五入)
文本框的字数限制功能jquery插件
JavaScript让IE浏览器event对象符合W3C DOM标准
为jquery.ui.dialog 增加“在当前鼠标位置打开”的功能
为jquery.ui.dialog 增加“自动记住关闭时的位置”的功能
js版扫雷实现代码 原理不错
鼠标跟随的文字变动效果
JS 判断undefined的实现代码
Ext grid 添加右击菜单
javascript或asp实现的判断身份证号码是否正确两种验证方法
javascript 获取元素位置的快速方法 getBoundingClientRect()
javascript 面向对象继承
JavaScript 地震特效
jquery.ui.progressbar 中文文档
JS 拖动效果实现代码 比较简单
JS小框架 fly javascript framework
JS在IE和FF下attachEvent,addEventListener学习笔记
js 对联广告、漂浮广告封装类(IE,FF,Opera,Safari,Chrome
js 动态选中下拉框

Javascript 中的 JavaScript实例:mini选择器实例代码详解


出处:互联网   整理: 软晨网(RuanChen.com)   发布: 2010-01-03   浏览: 79 ::
收藏到网摘: n/a

在网上发现一个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.})();