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的朋友应该还是可以看看的. 欢迎大家指出问题.

  github源码下载

参考书籍:

http://item.jd.com/10951037.html  ajax中post方法参考

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。