微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

构建一个类jq的函数库

2016-08-14 20:03 前端大全

(点击上方公众号,可快速关注)

来源:大搜车前端团队博客

链接:http://f2e.souche.com/blog/jqfree/


jqfree


虽然团队里用上了vue,但是某些情况下可能仍然需要操作DOM,或者是需要一些诸如变量类型判断、时间解析函数、url解析函数、浮点数四舍五入小数位和获取随机位数字符串的辅助函数。而本篇就是教你怎么构建这样一个山寨版的库,只要400行代码,你就能体验写一个库函数畅快的感觉!


jqfree core


var $ = function(selector, context) {  

    return new $.fn.init(selector, context);

};

 

$.fn = $.prototype;

 

$.fn.init = function(selector, context) {

    if (selector.nodeType === 1) {

        this[0] = selector;

        this.length = 1;

        return this;

    }

    var parent = context || document;

    var nodeList = parent.querySelectorAll(selector);

    this.length = nodeList.length;

    for (var i=0; i<this.length; i+=1) {

        this[i] = nodeList[i];

    }

    return this;

};

 

$.fn.init.prototype = $.fn;


我们需要一个包装着DOM Elements的伪数组,此伪数组对象使用原型链去挂载共享的DOM处理方法,原理如下图。



//选择器

$('body'); //返回$.fn.init {0: body, length: 1, selector: "body"}

$('.class');

$('#id');

$('#id .class');


extend


jqfree中的extend函数参照了prototype.js的实现方式,$.extend和$.fn.extend功能相同,也都是通过浅拷贝的方式,把第二个参数上的对象扩展添加到第二个参数的对象上,如果没有指定第二个参数,则会把第一个参数添加到this上。需要给DOM元素添加方法的话,使用$.fn.extend如$.fn.append,而需要给全局$对象添加扩展的话,使用$.extend,如$.ajax。


$.extend = $.fn.extend = function (destination, source) {

    //if source is not exist,copy the destination to this。

    if (typeof source === 'undefined') {

        source = destination;

        destination = this;

    }

    for (var property in source) {

        if (source.hasOwnProperty(property)) {

            destination[property] = source[property];

        }

    }

    return destination;

};


traverse


遍历jqfree对象中的DOM Elements。实际上是遍历了$.fn.init {0: body, length: 1, selector: "body"}这样的一个伪数组中的类似数组的那一部分。


$.fn.extend({

    each: function (func) {

        var i=0,

            length = this.length;

        for (; i<length; i+=1) {

            func.call(this[i], this[i], i);

        }

        return this;

    },

});


接受一个回调函数,其第一个参数为dom元素,第二个参数为序号,调用代码如


$('body').each(function(val, index){

    console.log(val, index)

});


DOM processor。


文档操作。添加了append,prepend,remove,empty的方法,功用同原版jquery。因为生成的$.fn.init是个包含DOM的伪数组,所以操作中就需要遍历这个数组做append操作,目的是为了让选中的所有DOM元素都append一遍。appendChild为DOM level2方法,从IE6开始就支持。


$.fn.extend({

    append: function (child) {

        if ($.isString(child)) {

            child = $(child)[0];

        }

        this.each(function(v, k) {

            v.appendChild(child);

        });

        child = null;

        return this;

    },

});


调用代码如


var element = document.createElement('div');  

$('body').append(element);


css


添加了css的方法,功用同原版jquery。现将css规则转为驼峰式,然后利用style属性插入,如background-color: #FFF,会被当作dom.style.backgroundColor = '#FFF'这样的插入。


$.fn.extend({

    css: function (cssRules, value) {

        //中划线转为驼峰式

        var transformHump = function (name) {

            return name.replace(/\-(\w)/g, function(all, letter){

                return letter.toUpperCase();

            });

        };

        if ($.isString(cssRules)) {

            if ($.isUndefined(value)) {

                return this[0].style[transformHump(cssRules)];

            } else {

                this[0].style[transformHump(cssRules)] = value;

            }

        } else {

            for (var i in cssRules) {

                this[0].style[transformHump(i)] = cssRules[i];

            }

        }

        return this;

 

    },

});


支持两种写法,参数1和参数2可以互为键值对,或者参数1为一个对象,另外这里只第一个dom元素的css规则生效。调用代码如


//设置第一个body元素的color值

$('body').css('color', '#FFF');

$('body').css({

    color: '#FFF',

    background: 'green'

});


DOM filter


添加了dom过滤的几个函数,如children、parent、siblings。返回出去的DOM对象会再次被$.fn.init对象包装。


$.fn.extend({

    children: function (selector) {

        return $(selector, this[0]);

    }

});


只对第一个DOM元素生效,调用代码如下:


$('body').children('.class'); //获取第一个body元素下的所有class名为'.class'的元素


attributes


获取属性,实现了attr,removeAttr,addClass,hasClass,removeClass,data,html这几个api,功能和jq相似。 拿addClass举例来说,classList为H5的API,不支持IE9及以下。所有被匹配的dom元素都会被addClass处理。


$.fn.extend({

    addClass: function (className) {

        this.each(function(v, k) {

            //please use 'v.className += className' if you need support IE9.

            v.classList.add(className);

        });

        return this;

    },

});


调用方式如下:


$('body').addClass('someClass');


event


事件操作。绑定事件使用on,取消绑定事件使用off,触发事件使用trigger。拿on举例,直接使用了addEventListener监听,不支持IE8及以下。需要支持IE8级以下的话,请使用attachEvent兼容。


$.fn.extend({

    on: function (event, func) {

        this.each(function(v, k) {

            //dom level 2,IE8 not support。

            v.addEventListener(event, func, false);

        });

        return this;

    },

});


第一个参数为事件名,第二个参数为回调,调用代码如下:


$('body').on('click', function(e){

    console.log('click');

})


effect


其他效果,鉴于动画用css3会更直观,所以这里只实现了show和hide两个方法。所有匹配的DOM元素都会被影响,这里只是简单设置了display属性为block或者none,有待改进。


$.fn.extend({

    show: function() {

        this.each(function() {

           this.style.display = 'block';

        });

        return this;

    },

});


调用代码如下:


$('body').hide();


ajax


抽离jsonp,$.jsonp独立于$.ajax,毕竟jsonp的原理和ajax完全没有关系,如果使用$.ajax的话有些误导别人。 $.ajax和$.jsonp方法最后都会返回一个Promise对象,此Promise参照了这里的方案。




$.ajax只接受一个对象作为参数,并且支持使用promise的写法,调用如下


$.ajax({

    url: '/test.json'

})

.then(function (d) {

    console.log(d);

 

    return $.ajax({

        url: '/test.json'

    })

}, function (d) {

    console.log(d);

})

.then(function (d) {

    console.log(d);

}, function (d) {

    console.log(d);

});

 

$.jsonp({

    url: '/test.json',

})

.then(function (d) {

    console.log(d);

 

    return $.jsonp({

        url: '/test.json'

    })

}, function (d) {

    console.log(d);

})

.then(function (d) {

    console.log(d);

}, function (d) {

    console.log(d);

});


注意,本地没法测试ajax函数,如果有需要请在此项目目录下运行node server.js,接着去打开test.html文件的关于ajax的注释,再去localhost:3000/test.html就能看到测试ajax的内容。


cookie


将增删改查cookie操作都用一个函数搞定


$.extend({

    cookie: function (cookieName, cookieValue, day) {

        var readCookie = function (name) {

            var arr,

                reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'),

                matched = document.cookie.match(reg);

            if(arr = matched) {

                return unescape(arr[2]);

            } else {

                return null;

            }

        };

        var setCookie = function (name, value, time) {

            var Days = time || 30;

            var exp = new Date();

            exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);

            document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString();

        };

        if (cookieName && cookieValue) {

            //set cookie

            setCookie(cookieName, cookieValue, day);

        } else if (cookieName && $.isNull(cookieValue)) {

            //delete cookie

            setCookie(cookieName, '', -1);

        } else if (cookieName) {

            //read cookie

            return readCookie(cookieName);

        }

    }

});


调用代码如下:


//添加cookie,前两个参数为cookie名和值,必填。第三个参数设置cookie有效时常,单位为天,可选。

$.cookie('test', 'content');

//读取cookie,只填第一个参数

$.cookie('test'); //"content"

//删除cookie, 第二个参数填null

$.cookie('test', null);


utils


添加了变量类型判断、时间解析函数、url解析函数、浮点数四舍五入小数位和获取随机位数字符串的辅助函数。


$.extend({

    isUndefined: function(obj) {

        return obj === void 0;

    },

    isNull: function(obj) {

        return obj === null;

    },

    isBoolean: function(obj) {

        return Object.prototype.toString.call(obj) === '[object Boolean]';

    },

    isNumber: function(obj) {

        return Object.prototype.toString.call(obj) === '[object Number]';

    },

    isString: function(obj) {

        return Object.prototype.toString.call(obj) === '[object String]';

    },

    isNaN: function(obj) {

        return obj !== obj;

    },

    isFunction: function(obj) {

        return typeof obj === 'function';

    },

    ......

});

$.extend({

    //$.parseTime(new Date().getTime(), 'YYYY-MM-DD hh:mm:ss')

    //result: "2016-08-03 16:14:12"

    parseTime: function (timeStamp, format) {

        var date = new Date(timeStamp);

        var o = {

            'M+' : date.getMonth() + 1, //month

            'D+' : date.getDate(), //day

            'h+' : date.getHours(), //hour

            'm+' : date.getMinutes(), //minute

            's+' : date.getSeconds(), //second

            'S' : date.getMilliseconds() //millisecond

        }

 

        if(/(Y+)/.test(format)) {

            format = format.replace(RegExp.$1,

                (date.getFullYear() + '').substr(4 - RegExp.$1.length));

        }

 

        for(var k in o) {

            if (new RegExp('('+ k +')').test(format)) {

                format = format.replace(RegExp.$1,

                    RegExp.$1.length == 1 ? o[k] : ('00'+ o[k]).substr((''+ o[k]).length));

            }

        }

        return format;

    },

 

    //$.parseUrl(location.href)

    //return an object contains the folling info.

    parseUrl: function (url) {

        var a =  document.createElement('a');

        a.href = url;

        return {

            source: url,

            protocol: a.protocol.replace(':',''),

            host: a.hostname,

            port: a.port,

            query: a.search,

            params: (function(){

                var ret = {},

                    seg = a.search.replace(/^\?/,'').split('&'),

                    len = seg.length, i = 0, s;

                for (;i<len;i++) {

                    if (!seg[i]) { continue; }

                    s = seg[i].split('=');

                    ret[s[0]] = s[1];

                }

                return ret;

            })(),

            file: (a.pathname.match(/\/([^\/?#]+)$/i) || [,''])[1],

            hash: a.hash.replace('#',''),

            path: a.pathname.replace(/^([^\/])/,'/$1'),

            relative: (a.href.match(/tps?:\/\/[^\/]+(.+)/) || [,''])[1],

            segments: a.pathname.replace(/^\//,'').split('/')

        };

    },

 

    //$.toFixedFloat(15.658, 2)

    //result: 15.66

    toFixedFloat: function (value, precision) {

        var power = Math.pow(10, precision || 0);

        return String(Math.round(value * power) / power);

    },

 

    //for generate random string

    //$.generateRandomAlphaNum(5)

    //random result: like "rc3sr".

    generateRandomAlphaNum: function (len) {

        var rdmString = '';

        for (; rdmString.length < len; rdmString += Math.random().toString(36).substr(2));

        return rdmString.substr(0, len);

    }

});


调用如下:


//参数1是时间戳,参数2是格式,年为Y,月为M,日为D,时h,分m,秒s,毫秒S,注意大小写,多余的位数补0

$.parseTime(new Date().getTime(), 'YYYY-MM-DD hh:mm:ss'); //"2016-08-03 16:14:12"。

 

//参数为url链接

$.parseUrl(location.href); //返回一个带诸多url信息的对象。

 

//参数1是目标浮点数,参数2是保留到第几位小数

$.toFixedFloat(15.658, 2); //四舍五入到两位小数:15.66

 

//参数为生成随机的字符串长度

$.generateRandomAlphaNum(5); //如"rc3sr"


说明


jqfree纯粹研究用,不考虑诸多兼容。算上注释也就只有400行,可以简单研究一下其代码是如何构建的。从jq毕业一阵子了,总得写点东西纪念下老伙计。github地址在这里,有启发的话请不吝给我的github点赞。


参考


  • 张鑫旭 jQuery诞生记-原理与机制

  • 阮一峰 如何做到jquery-free

  • extend 方法在js框架中的设计

  • 刘镇维 Promise简单实现(正常思路版)

  • JS设置cookie、读取cookie、删除cookie


【今日微信公号推荐↓】

更多推荐请看值得关注的技术和设计公众号


其中推荐了包括技术设计极客 和 IT相亲相关的热门公众号。技术涵盖:Python、Web前端、Java、安卓、iOS、PHP、C/C++、.NET、Linux、数据库、运维、大数据、算法、IT职场等。点击《值得关注的技术和设计公众号》,发现精彩!


 
前端大全 更多文章 在JavaScript中借用方法 Vue.js 60 分钟快速入门 我们为什么要尝试前后端分离 移动端样式小技巧 JavaScript 模块(2):模块打包
猜您喜欢 大师经典 — Martin Fowler的"持续集成" 微博推荐的 storm 实践 前端Talk月刊第四期 解秘武岳峰资本:凭什么抢夺美国矽成半导体? 架构漫谈(八):从架构的角度看如何写好代码