jqfree
雖然團隊裡用上了vue,但是某些情況下可能仍然需要操作DOM,或者是需要一些諸如變數型別判斷、時間解析函式、url解析函式、浮點數四捨五入小數位和獲取隨機位數字符串的輔助函式。而本篇就是教你怎麼構建這樣一個山寨版的庫,只要400行程式碼,你就能體驗寫一個庫函式暢快的感覺!
jqfree core
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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處理方法,原理如下圖。
1 2 3 4 5 |
//選擇器 $('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。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$.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"}
這樣的一個偽陣列中的類似陣列的那一部分。
1 2 3 4 5 6 7 8 9 10 |
$.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元素,第二個引數為序號,呼叫程式碼如
1 2 3 |
$('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開始就支援。
1 2 3 4 5 6 7 8 9 10 11 12 |
$.fn.extend({ append: function (child) { if ($.isString(child)) { child = $(child)[0]; } this.each(function(v, k) { v.appendChild(child); }); child = null; return this; }, }); |
呼叫程式碼如
1 2 |
var element = document.createElement('div'); $('body').append(element); |
css
新增了css的方法,功用同原版jquery。現將css規則轉為駝峰式,然後利用style屬性插入,如background-color: #FFF
,會被當作dom.style.backgroundColor = '#FFF'
這樣的插入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
$.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規則生效。呼叫程式碼如
1 2 3 4 5 6 |
//設定第一個body元素的color值 $('body').css('color', '#FFF'); $('body').css({ color: '#FFF', background: 'green' }); |
DOM filter
新增了dom過濾的幾個函式,如children、parent、siblings。返回出去的DOM物件會再次被$.fn.init物件包裝。
1 2 3 4 5 |
$.fn.extend({ children: function (selector) { return $(selector, this[0]); } }); |
只對第一個DOM元素生效,呼叫程式碼如下:
1 |
$('body').children('.class'); //獲取第一個body元素下的所有class名為'.class'的元素 |
attributes
獲取屬性,實現了attr,removeAttr,addClass,hasClass,removeClass,data,html這幾個api,功能和jq相似。 拿addClass舉例來說,classList為H5的API,不支援IE9及以下。所有被匹配的dom元素都會被addClass處理。
1 2 3 4 5 6 7 8 9 |
$.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; }, }); |
呼叫方式如下:
1 |
$('body').addClass('someClass'); |
event
事件操作。繫結事件使用on,取消繫結事件使用off,觸發事件使用trigger。拿on舉例,直接使用了addEventListener監聽,不支援IE8及以下。需要支援IE8級以下的話,請使用attachEvent相容。
1 2 3 4 5 6 7 8 9 |
$.fn.extend({ on: function (event, func) { this.each(function(v, k) { //dom level 2,IE8 not support。 v.addEventListener(event, func, false); }); return this; }, }); |
第一個引數為事件名,第二個引數為回撥,呼叫程式碼如下:
1 2 3 |
$('body').on('click', function(e){ console.log('click'); }) |
effect
其他效果,鑑於動畫用css3會更直觀,所以這裡只實現了show和hide兩個方法。所有匹配的DOM元素都會被影響,這裡只是簡單設定了display屬性為block或者none,有待改進。
1 2 3 4 5 6 7 8 |
$.fn.extend({ show: function() { this.each(function() { this.style.display = 'block'; }); return this; }, }); |
呼叫程式碼如下:
1 |
$('body').hide(); |
ajax
抽離jsonp,$.jsonp獨立於$.ajax,畢竟jsonp的原理和ajax完全沒有關係,如果使用$.ajax的話有些誤導別人。 $.ajax和$.jsonp方法最後都會返回一個Promise物件,此Promise參照了這裡的方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
var Promise = function (fn) { var state = 'pending'; var doneList = []; var failList= []; this.then = function(done ,fail){ switch(state){ case 'pending': doneList.push(done); //每次如果沒有推入fail方法,我也會推入一個null來佔位 failList.push(fail || null); return this; break; case 'fulfilled': done(); return this; break; case 'rejected': fail(); return this; break; } } function tryToJson(obj) { var value; try { value = JSON.parse(obj); } catch (e) { value = obj; } return value } function resolve(newValue){ state = 'fulfilled'; setTimeout(function(){ var value = tryToJson(newValue); for (var i = 0; i < doneList.length; i++){ var temp = doneList[i](value); if (temp instanceof Promise) { var newP = temp; for (i++; i < doneList.length; i++) { newP.then(doneList[i], failList[i]); } } else { value = temp; } } }, 0); } function reject(newValue){ state = 'rejected'; setTimeout(function(){ var value = tryToJson(newValue); var tempRe = failList[0](value); //如果reject裡面傳入了一個promise,那麼執行完此次的fail之後,將剩餘的done和fail傳入新的promise中 if(tempRe instanceof Promise){ var newP = tempRe; for (i=1;i<doneList.length;i++) { newP.then(doneList[i],failList[i]); } } else { //如果不是promise,執行完當前的fail之後,繼續執行doneList value = tempRe; doneList.shift(); failList.shift(); resolve(value); } }, 0); } fn(resolve,reject); }; $.extend({ ajax: function (opts) { var xhr = new XMLHttpRequest(), type = opts.type || 'GET', url = opts.url, success = opts.success, error = opts.error, params; params = (function(obj){ var str = ''; for(var prop in obj){ str += prop + '=' + obj[prop] + '&' } str = str.slice(0, str.length - 1); return str; })(opts.data); type = type.toUpperCase(); if (type === 'GET') { url += url.indexOf('?') === -1 ? '?' + params : '&' + params; } xhr.open(type, url); if (type === 'POST') { xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded"); } xhr.send(params ? params : null); //return promise return new Promise(function (resolve, reject) { //onload are executed just after the sync request is comple, //please use 'onreadystatechange' if need support IE9- xhr.onload = function () { if (xhr.status === 200) { resolve(xhr.response); } else { reject(xhr.response); } }; }); }, jsonp: function (opts) { //to produce random string var generateRandomAlphaNum = function (len) { var rdmString = ''; for (; rdmString.length < len; rdmString += Math.random().toString(36).substr(2)); return rdmString.substr(0, len); } var url = opts.url, callbackName = opts.callbackName || 'jsonpCallback' + generateRandomAlphaNum(10), callbackFn = opts.callbackFn || function () {}; if (url.indexOf('callback') === -1) { url += url.indexOf('?') === -1 ? '?callback=' + callbackName : '&callback=' + callbackName; } var eleScript= document.createElement('script'); eleScript.type = 'text/javascript'; eleScript.id = 'jsonp'; eleScript.src = url; document.getElementsByTagName('HEAD')[0].appendChild(eleScript); // window[callbackName] = callbackFn; //return promise return new Promise(function (resolve, reject) { window[callbackName] = function (json) { resolve(json); } //onload are executed just after the sync request is comple, //please use 'onreadystatechange' if need support IE9- eleScript.onload = function () { //delete the script element when a request done。 document.getElementById('jsonp').outerHTML = ''; eleScript = null; }; eleScript.onerror = function () { document.getElementById('jsonp').outerHTML = ''; eleScript = null; reject('error'); } }); } }); |
$.ajax只接受一個物件作為引數,並且支援使用promise的寫法,呼叫如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
$.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操作都用一個函式搞定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
$.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); } } }); |
呼叫程式碼如下:
1 2 3 4 5 6 |
//新增cookie,前兩個引數為cookie名和值,必填。第三個引數設定cookie有效時常,單位為天,可選。 $.cookie('test', 'content'); //讀取cookie,只填第一個引數 $.cookie('test'); //"content" //刪除cookie, 第二個引數填null $.cookie('test', null); |
utils
新增了變數型別判斷、時間解析函式、url解析函式、浮點數四捨五入小數位和獲取隨機位數字符串的輔助函式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
$.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 3 4 5 6 7 8 9 10 11 |
//引數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點贊。