select----一个仿jquery的库
最近写了一个功能仿jquery的库, 叫select, 因为只写了较常用的功能, 所以体积较小(压缩下来10多K), 兼容性还行, ie6暂时没发现问题, 因为时间和技术有限, 很可能有较多bug, 欢迎指出 . demo演示, 源码: github源码下载
虽然功能和用法仿jquery, 但原理有很多不同. 大概结构如下:
1 !function(window){ 2 var select = function( selector ){ 3 this.set = []; //dom元素集合 4 this.init( selector ); 5 }, 6 s = function( selector ){ 7 return new select( selector ); 8 }; 9 10 select.prototype = { 11 /*select对象方法*/ 12 } 13 14 /*内部私有函数*/ 15 16 s.fn = { 17 /*常用方法*/ 18 } 19 var attr; 20 for(attr in s.fn) s[attr] = s.fn[attr]; 21 window.s = s; 22 }( window );
从结构可以看出, 为了防止污染全局变量, 整个select库是一个自执行函数, 为了和jq区分开, 这里使用的 s 而不是 $ 来调用, 每次调用如 s(".box>li") 的时候会返回一个新的 select 对象, 并且会将所有匹配到的dom元素存放在返回 select 的 set 数组中然后调用 select 原型对象中的方法. 因为要链式操作, 所以在 select 的方法中添加 return this 以便每次能执行完一个方法之后还能得到原来 select 对象.
s 是一个函数对象, 既可以调用它生成 select 对象, 也可以调用它自己的方法, 一些和dom操作关系不大的方法可以作为 s 的方法, 在 s.fn 中声明, 这样不用每次调用 s 生成一个多余的 select 对象, 比如 ajax 的 .get() 和 .post() 方法, 还有 es5 中的 .forEach() , .filter() 等方法. 最后将 s.fn 中所有方法的引用放到 s 中, 并将s放到全局环境中.
选择器
因为 select 库需要兼容较低版本 ie , 用 document.querySelectorAll() 肯定不行, 所以我自己的实现方法是用两个函数从复杂选择器中返回匹配的dom元素集合, 实现原理如下:
1 function divide(selector){ 2 if( typeof selector === "object" ){ 3 this.set.length = 0; 4 selector.length ? this.set.concat(selector) : this.set.push(selector); 5 } 6 else if( selector === "document" ) this.set.push(doc); 7 else if( selector === "body" ) this.set.push(body); 8 else if( typeof selector === "string"){ 9 selector = s.trim(selector, " "); //去掉两端空格 10 if( selector === "*" ){ 11 var temp = s.filter( getTag(doc, "*"), function(i, e){ 12 return commonTag(e) ? true : false; 13 } ); 14 this.set = s.concat(this.set, temp ); 15 } 16 else{ 17 //先用逗号分开 18 selector = selector.split(/\s*,\s*/g); 19 for( var i=0, m=selector.length; i<m; i++ ){ 20 //将逗号分开的逐个分成子项, 例如:"#main", " li", ".item", "[type=text]" 21 selector[i] = selector[i].match( />?\s*?([#\.]?[a-z\*][a-z0-9_\-=\!]*|\[[a-z0-9_]+?[!\$]?=.+?\]|:[a-z][a-z0-9_\(\)‘"]+)/gi ); 22 23 for(var j=0, n=selector[i].length; j<n; j++){ 24 detect.call(this, selector[i][j]); 25 } 26 this.set = s.concat( this.set, this.cop ); 27 this.cop.length = 0; 28 } 29 } 30 } 31 this.set = unique(this.set); //unique函数传入一个数组, 返回一个新数组, 这个新数组中每个元素都是唯一的 32 };
由于选择器代码较多, 而且select对象通常只是初始化的时候调用一下, 所以将 divide 放在select外面, 调用的时候需要 divide.call(this, selector)
divide函数主要作用是拆分选择字符串, 会对传入参数先判断. 若是形如 s("#box ul .item input:disabled, .show li:gt(3), .foot div>a ") 的字符串, 则会先根据"," 进行拆分成3段, 然后将第1段中 "#box ul .item input:disabled" 用正则表达式拆分成 ["#box", " ul", " .item", " input", ":disabled"] ,原变量引用新数组(上面21行), 后面的以此类推. 每次匹配的最终数组, 传入detect 函数中, 因为detect函数太长, 大概结构如下:
function detect(str){ //调用次函数之前,必须让this.cop=this.set(如果是过滤), 并且必须保证this.cop和this.set不指向同一个数组 var i, temp, tmp, cop = this.cop , This = this; this.cop = []; //如果匹配到的是" > .item" 格式, 则将其中连续空格替换">.item", 若是" .item"则不替换 //如果遇到 " , #main" 替换成 ",#main" str = str.replace(/^\s*([>#])\s*([^,>]+)$/gi, ‘$1$2‘); //如果开头有超过两个空格,如" .item",就替换成" .item" if( str.indexOf(" ") > -1 ) str = str.replace(/^\s+/g, ‘ ‘); if( str.charAt(0) === " " && s.trim(str, " ").charAt(0) !== "," ){ //针对于内部所有节点, 前面是" "或者">"的str进去第一步都是先去第一个字符 str = s.trim(str, " "); if( str.charAt(0) === "#" ) this.cop.push( getId( str.substring(1) ) ); else if( str.charAt(0) === "." ){ s.each(cop, function(i, e){ if( !s.old_ie ){ //若是现代浏览器 This.cop = s.concat(This.cop, getCls(cop[i], str.substring(1))); } else{ temp = s.filter( getTag( cop[i], "*" ) , function(i, e){ hasCls(e, str.substring(1)); }); This.cop = s.concat(This.cop, temp); } }); } else if( str.charAt(0) == "*" ){ s.each(cop, function(i, e){ temp = s.filter(getTag(e, "*") , function(i, e){ return e.nodeType === 1 ? true : false; }); This.cop = s.concat(This.cop, temp); }); } else if( str.charCodeAt(0) >= 97 && str.charCodeAt(0) <= 122 ){ s.each(cop, function(i, e){ This.cop = s.concat( This.cop, getTag(cop[i], str) ); }); } else{ //当" [type=text]"类似字符串的时候, 或者" :checked"类似结构, 统一设置cop到起内部节点 s.each(cop, function(i, e){ This.cop = s.concat( This.cop, getTag(e, "*") ); }); arguments.callee.call( This, str ); } } else if( str.charAt(0) === ">" ){ str = str.substring(1); if( str.charAt(0) === "#" ) this.cop.push( getId( str.substring(1) ) ); else if( str.charAt(0) === "." ){ s.each(cop, function(i, e){ temp = s.filter( e.childNodes, function(i, e){ return ( e.nodeType === 1 && hasCls(e, str.substring(1)) ) ? true : false; } ); This.cop = s.concat( This.cop, temp ); }); } else if( str.charAt(0) == "*" ){ s.each(cop, function(i, e){ temp = s.filter(e.childNodes, function(i, e){ return e.nodeType === 1 ? true : false; }); This.cop = s.concat(This.cop, temp); }); } else if( str.charCodeAt(1) >= 97 && str.charCodeAt(1) <= 122 ){ s.each(cop, function(i, e){ temp = s.filter( e.childNodes , function(i, e){ return e.nodeName === str.substring(0).toUpperCase() ? true : false; } ); This.cop = s.concat( This.cop, temp ); }); } else{ s.each(cop, function(i, e){ temp = s.filter(e.childNodes, function(i, e){ return e.nodeType === 1 ? true : false; }); This.cop = s.concat( This.cop, temp ); }); arguments.callee.call( This, str ); } } else if( str === "*" ) this.cop = cop; else{ //前面没有" "和">"的情况 if( str.charAt(0) === "#" ){ if( cop.length === 0 ) this.cop.push( getId(str.substring(1)) ); else this.cop = s.filter(cop, function(i, e){ return e.id === str.substring(1) ? true : false; }); } /* ........省略的部分代码.........*/ } }; }
每次传入 detect 的 str 是形如 "#box", " ul", " input", ":disabled", 如果str的第一个字符是否是" "或">", 则会将 this.set 中所有dom元素替换成他们的内部元素或直接子元素, 如果不是" "和">"开头的 str, 则直接将 this.set 进行过滤, 每次得到的元素存在在 this.cop 这个临时数组中, 然后合并到 this.set, 然后让 this.cop 变为空数组(在divide函数中), 再次去获取另一段 ".show li:gt(3)".所得到的dom元素, 然后再次放到 this.cop 中, 再次合并到 this.set. 不光是 s(selector) 可以调用选择器, select的.find, .filter等通常可以调用, 实现代码复用.
从上面的 divide 和 detect 函数可以看出, 大量使用了 s.concat, 如果通过 document.getElementsByTagName(tag) 得到的集合并非数组, 而是一个HTMLCollection的对象. 因此不能直接使用 数组自带的concat, s.concat需要传入两个数组, 会将两个数组中的元素放到新数组中并返回, HTMLCollection也不例外.
select的其他方法基本上都是内部执行一个遍历方法, 遍历 this.set 数组, 然后给每个元素执行回调函数, 例如:
select.prototype.html = function(v){ var This = this; if( typeof v === "undefined" ) return this.set[0].innerHTML; else{ s.each(this.set, function(i, e){ e.innerHTML = typeof v === "function" ? v.call(This) : v ; }); return this; } }
其他方法同理.
事件
select的事件写的非常简单, 并未走事件队列, 只是一个事件绑定方法:
select.prototype.on = function(type, fn){ s.each(this.set, function(i, e){ bind(e, type, fn); }); return this; } function bind(obj, type, fn){ if( obj.attachEvent ){ obj.attachEvent("on" + type, function(){ fn.call(obj); }); } else obj.addEventListener(type, fn, false); }
得到父级,兄弟或子节点
select内部有 .next() .prev() .children() 和jquery用法一样, 还有支持传入字符串进行过滤的 .siblings() .parent() .parents(), 这些会用到detect函数进行过滤.
AJAX
ajax和dom操作无关, 因此放在 s 对象中, 只包含 get 和 post, 并未对参数类型做过多判断, 因此调用方式比较固定, 例如:
s.get("a.php?n=5", function(str){ alert("发送成功!"); }, function(){ alert("发送失败!"); }); s.post("a.php", {uname:"abc", token:"abcdefg"}, function(str){ alert("发送成功!"); }, function(){ alert("发送失败!"); });
post方法内部代码:
s.fn = { post: function(url, json, fnSuccess, fnFail){ var xhr = s.getXHR(); xhr.open(‘POST‘, url, true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.send( s.serial(json) ); var This = this; xhr.onreadystatechange = function(){ if(xhr.readyState == 4){ if( (xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) fnSuccess.call(This, xhr.responseText); else if(fnFail) fnFail.call(This, xhr.status); } } }, getXHR: function(){ //得到xhr对象 return window.XMLHttpRequest ? new window.XMLHttpRequest : new ActiveXObject("Microsoft.XMLHTTP"); }, serial: function(json){ //传入json, 返回序列化字符串 var k=‘‘, v=‘‘, res=‘‘, i=0; for(attr in json){ json[attr] = json[attr] + ‘‘; k=‘‘, v=‘‘; for(i=0; i<attr.length; i++){ if(attr.charAt(i)==‘ ‘) k+=‘+‘; else k+=encodeURIComponent(attr.charAt(i)); } for(i=0; i<json[attr].length; i++){ if(json[attr].charAt(i)==‘ ‘) v+=‘+‘; else v+=encodeURIComponent(json[attr].charAt(i)); } res = res + k + ‘=‘ + v + ‘&‘; } return s.trim(res, "&"); } }
s 对象中并没有自带 jsonp 跨域的方法.
动画
select对象中自带有一个 animate 的方法用于实现dom元素的动画, 同一个 select 对象, 不同地方添加多个动画, 会放在内部的动画队列(this.aniQueue)中, 每次遇到 .animate方法, 则会将其 push 进 this.aniQueue中, 如果一个动画执行完成, 则将其 shift 出去.
1 select.prototype.aniRunning = false; 2 select.prototype.animate = function(json, time, callback){ 3 if( this.aniRunning ) return this; 4 time = time || 500; 5 var This = this; 6 if( !this.aniQueue ) this.aniQueue = []; 7 this.aniQueue[this.aniQueue.length] = function(){ 8 s.each(This.set, function(i, e){ 9 animate.call(e, This, e, json, time, callback); 10 }); 11 }; 12 if( this.aniQueue.length === 1 ) this.aniQueue[0](); 13 return this; 14 }
select同时有个aniRunning的属性, 用来判断动画队列中的动画是否已经全部完成, 防止一个动画还未完成被重复执行产生的问题. 第9行中的animate函数是一个独立的动画函数, 因为代码较多, 下一篇博文单独说.
如果想在select上添加一些功能, 比如实现jsonp或添加一些其他常用方法, 可以通过 s.extend({ /*...*/ })方式给s对象添加, 或者通过 select.extend({ /*...*/ })方法对select对象添加.
到目前为止, 主要功能的实现方法说的差不多了, 当然select.js 中还有非常多的简单的方法, 就不一一列举, 有兴趣的朋友可以看源码, 秒懂的东西. select.js是我写来玩的, 目的是让其尽量小, 保留主要功能, 能兼容老的ie. 因为技术有限, 功能和稳定性比jquery差很远, 不过对于初学js的朋友应该还是可以看看的. 欢迎大家指出问题.
参考书籍:
http://item.jd.com/10951037.html ajax中post方法参考
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。