js模版对于一个健壮的组件库来说,至关重要。犹如建筑一栋大楼,模版就是钢筋,数据就是水泥,事件就是布线和弱电。本文将从一个小函数讲起,然后重点探讨js模版的实现模式、易用性、可扩展性,然后再对ext的模版体系做简单分析。
由于工作原因,本人一直在维护一个datagrid组件,datagrid的需求千变万化,大概60%的需求都是对单元格的处理,刚刚开始的时候需要一个功能就加一个功能,比如单元格需要根据数据改变背景颜色,于是我便在表格生成之后直接操作dom,比如带checkbox的datagrid,翻页后需要保存已选状态,于是我便在表格生成之后查找checkbox然后再选中。需要在增加,datagrid也慢慢变的臃肿起来,不堪重负,leader也决定开始重构了。在重构之初,我便决定,在表格生成之前把需要处理的都完成,这样就可以节省查询dom的时间。这样以来,前期创建需要处理的逻辑就会很多,所以这里就需要一个很完善的模版体系来做支持,否则玩到最后又会变的很臃肿。
于是我尝试着写了一个简单的基于对象模式的模版,代码如下:
/** * * 对象模式创建模版 * * @param {Array} attrs 生成的节点数组 * @param {String} type 类型 * @param {Array|Object} attr 属性 * @param {Array|Object} child 子节点 * @param {Number} num 子节生成个数 * @param {Function} func 处理函数 * @param {Array} data 数据 * * @param {Element|String} target */ var tpl = function(ats, target) { target = fast.id(target); if (fast.isArray(ats) && ats.length > 0 && target.appendChild) { for (var i = 0, len = ats.length; i < len; i++) { var attrs = ats[i], tag = attrs.tag, attr = attrs.attr || {}, data = attrs.data, func = attrs.func, child = attrs.child, num = attrs.num ? attrs.num : 1, j = 0; var fragment = document.createDocumentFragment(); for (; j < num; j++) { var isFunc = false; if (data) { if (child) { if (fast.isArray(child)) { for (var k = 0, l = child.length; k < l; k++) { child[k].data = data[j]; } } else { child.data = data[j]; } } else { if (func) { attr = func(j, attr, data); isFunc = true; } else { data = fast.values(data); attr.text = data[j]; } } } (isFunc === false) && func && ( attr = func(j, attr, data)); var nodes = fast.node(tag, attr); fragment.appendChild(nodes); child && tpl(child, nodes); } target.appendChild(fragment); } } };
另外创建了一个基类,这个基类后面的例子都会用到,希望读者注意。
var doc = window.document, _toString = Object.prototype.toString; var fast = { isString : function(obj) { return !!(obj === '' || (obj && obj.charCodeAt && obj.substr)); }, isNumber : function(obj) { return _toString.call(obj) === '[object Number]'; }, isArray : [].isArray || function(obj) { return _toString.call(obj) === '[object Array]'; }, isObject : function(obj) { return obj == null ? String(obj) == 'object' : _toString.call(obj) === '[object Object]' || true; }, isEmptyObject : function(obj) { for (var name in obj) { return false; } return true; }, getID : function() { var num1 = new Date().getTime(); var num2 = parseInt(Math.random() * 100000, 10); return num1 + num2; }, id : function(id) { if (this.isString(id)) { return doc.getElementById(id); } else if (id.nodeType) { return id; } return; }, html : function(el, html) { el = this.id(el); if (html) { if (el != null && 'innerHTML' in el) { el.innerHTML = html; } } else { return el.innerHTML; } }, values : function(obj) { var ret = []; for (var key in obj) { ret.push(obj[key]); } return ret; }, setCssText : function(el, cssText) { el.style.cssText = cssText; }, setAttr : function(element, attrObj) { var me = this, mapObj = { "class" : function() { element.className = attrObj["class"]; }, "style" : function() { me.setCssText(element, attrObj["style"]); }, "text" : function() { if (attrObj["text"].nodeType) { element.appendChild(attrObj["text"]); } else { element.appendChild(document.createTextNode(attrObj["text"])); } } } for (p in attrObj) { if (mapObj[p]) { mapObj[p](); } else { element.setAttribute(p, attrObj[p]); } } }, node : function(type, attrObj) { var element = doc.createElement(type); if (!this.isEmptyObject(attrObj)) { this.setAttr(element, attrObj); } return element; }, testTime : function(get_as_float) { var now = new Date().getTime() / 1000; var s = parseInt(now, 10); return (get_as_float) ? now : (Math.round((now - s) * 1000) / 1000) + ' ' + s; }, //ext**********************************/ _indexOf : Array.prototype.indexOf, inArray : function(elem, arr, i) { var len; if (arr) { if (this._indexOf) { return this._indexOf.call(arr, elem, i); } len = arr.length; i = i ? i < 0 ? Math.max(0, len + i) : i : 0; for (; i < len; i++) { if ( i in arr && arr[i] === elem) { return i; } } } return -1; }, isDate : function(o) { return (null != o) && !isNaN(o) && ("undefined" !== typeof o.getDate); }, Format : {}, decodeHTML : function(str) { str = String(str).replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, "&"); //处理转义的中文和实体字符 return str.replace(/([\d]+);/g, function(_0, _1) { return String.fromCharCode(parseInt(_1, 10)); }); }, apply : function(object, config, defaults) { if (defaults) { this.apply(object, defaults); } var enumerables = true, enumerablesTest = { toString : 1 }; for (i in enumerablesTest) { enumerables = null; } if (enumerables) { enumerables = ['hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'constructor']; } if (object && config && typeof config === 'object') { var i, j, k; for (i in config) { object[i] = config[i]; } if (enumerables) { for ( j = enumerables.length; j--; ) { k = enumerables[j]; if (config.hasOwnProperty(k)) { object[k] = config[k]; } } } } return object; } };
在模版使用之前,我们需要预先定义一组数据,这组数据后面的几个模版体系都会用到:
var data = [ {name : "test1",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "1"}, {name : "test2",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "2"}, {name : "test3",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "3"}, {name : "test4",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "4"}, {name : "test5",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "5"}, {name : "test6",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "6"}, {name : "test7",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "7"}, {name : "test8",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "8"}, {name : "test9",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "9"}, {name : "test10",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "10"} ]; for(var i = 10; i < 1000; i++){ data.push({name : "test"+i,sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : i}); }
这个模版的使用事例如下:
var td = [{tag:"td",num:5,attr:{text:"text"}}]; var tr = [{tag:"tr",num:1000,child:td,data:data}]; var tbody = [{tag:"tbody",child:tr}]; tpl([{tag:"table",attr:{style:"width:100%",border:"1"},child:tbody}],"example");
当然,您也可以这样写:
tpl([{tag:"table",attr:{style:"width:100%",border:"1"}, child:[{tag:"tbody", child:[{tag:"tr",num:1000, child:[{tag:"td",num:5,attr:{text:"text"}}], data:data}]} ]}], "example");
该模版的核心思路就是递归创建dom,支持对每个dom绑定数据,支持外部函数调用(helper),支持内嵌数据处理,支持一次创建多个平级dom。
对于一个组件库来说,感觉这样很完美了,于是我兴致冲冲的想拿其他的模版体系做对比。找了一大圈,发现别人玩模版不是这样玩的,大部分都是先拼装字符串,然后再放到一个闭包里来处理,再返回。
于是我模仿着别人写了一个原型,代码如下:
/* * 字符串模式创建模版 * */ var tp = function(str, data) { var str = fast.id(str) ? fast.html(str) : str, str = str.replace(/<\#(\s|\S)*?\#>/g, function(p) { return p.replace(/("|\\)/g, "\\$1").replace("<#", '_s.push("').replace("#>", '");').replace(/<\%([\s\S]*?)\%>/g, '",$1,"') }).replace(/\r|\n/g, ""), keys = [], values = [], i; for (i in data) { keys.push(i); values.push(data[i]); } return (new Function(keys, "var _s=[];" + str + " return _s;") ).apply(null, values).join(""); };
调用方式大致如下:
ext模版分两种,一种是templete,数据模型只处理字符串和数组,另外一种是xtemplete,可以处理对象,上面剥离的是xtemplete。在剥离的过程不禁惊叹js的精髓,原来js可以这样写!ext的大神们很精巧的拼装了一个内置函数,核心函数在generate和parse,generate负责组装,parse负责解析。
然后测试了一下,速度更是惊人,几乎和测试字符串模式(tp函数)跑平!那么多的判断分支,神了,再膜拜一下ext。
细嚼完ext,于是又回头看了一下jquery,由于时间问题没来得及剥离,粗略的写了一个用例。
测试中,jquery的用时在最长的,因为没有剥离出内核,所以不能妄加评论。但是它的写法是最精简的,值得学习和借鉴。
全部测试数据如下(单位:秒):
chrome: