js模版深度解析
2012-09-04 16:37
288 查看
js模版对于一个健壮的组件库来说,至关重要。犹如建筑一栋大楼,模版就是钢筋,数据就是水泥,事件就是布线和弱电。本文将从一个小函数讲起,然后重点探讨js模版的实现模式、易用性、可扩展性,然后再对ext的模版体系做简单分析。
由于工作原因,本人一直在维护一个datagrid组件,datagrid的需求千变万化,大概60%的需求都是对单元格的处理,刚刚开始的时候需要一个功能就加一个功能,比如单元格需要根据数据改变背景颜色,于是我便在表格生成之后直接操作dom,比如带checkbox的datagrid,翻页后需要保存已选状态,于是我便在表格生成之后查找checkbox然后再选中。需要在增加,datagrid也慢慢变的臃肿起来,不堪重负,leader也决定开始重构了。在重构之初,我便决定,在表格生成之前把需要处理的都完成,这样就可以节省查询dom的时间。这样以来,前期创建需要处理的逻辑就会很多,所以这里就需要一个很完善的模版体系来做支持,否则玩到最后又会变的很臃肿。
于是我尝试着写了一个简单的基于对象模式的模版,代码如下:
另外创建了一个基类,这个基类后面的例子都会用到,希望读者注意。
View Code
调用方式:
ext模版分两种,一种是templete,数据模型只处理字符串和数组,另外一种是xtemplete,可以处理对象,上面剥离的是xtemplete。在剥离的过程不禁惊叹js的精髓,原来js可以这样写!ext的大神们很精巧的拼装了一个内置函数,核心函数在generate和parse,generate负责组装,parse负责解析。
然后测试了一下,速度更是惊人,几乎和测试字符串模式(tp函数)跑平!那么多的判断分支,神了,再膜拜一下ext。
细嚼完ext,于是又回头看了一下jquery,由于时间问题没来得及剥离,粗略的写了一个用例。
测试中,jquery的用时在最长的,因为没有剥离出内核,所以不能妄加评论。但是它的写法是最精简的,值得学习和借鉴。
全部测试数据如下(单位:秒):
chrome:
测试对象的模式:用时0.04700016975402832
测试字符串模式:用时0.03299999237060547
测试extTmplete模式:用时0.03299999237060547
测试jquerytmpl模式:用时0.11500000953674316
ie9:
测试对象的模式:用时0.44099998474121093
测试字符串模式:用时0.03399991989135742
测试extTmplete模式:用时0.032000064849853516
测试jquerytmpl模式:用时0.3899998664855957
走了一圈之后再回顾自己写的模版,发现了自己的很多不足,急于结果的实现,对过程的把控没做合理的布局,实现上还需要做进一步推敲。
总结:
优秀js模版几个关键因素:
一、支持多级数据,无论ext还是jquery都支持。比如data数据,模版内可以做data.param1循环也可以做data.param2循环。
二、支持模版助手helper,可以通过助手任意处理模版里的控件,给模版提供灵活性。
三、有完善的容错机制。
四、支持内嵌循环。
五、易用性和速度效率,jquery的模版为什么会使用那么广是有原因的,用户能不能接受才是最关键的。
由于工作原因,本人一直在维护一个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); } } };
另外创建了一个基类,这个基类后面的例子都会用到,希望读者注意。
View Code
extpl = { constructor: function(html) { var me = this, args = arguments, buffer = [], i = 0, length = args.length, value; me.initialConfig = {}; if (length > 1) { for (; i < length; i++) { value = args[i]; if (typeof value == 'object') { fast.apply(me.initialConfig, value); fast.apply(me, value); } else { buffer.push(value); } } html = buffer.join(''); } else { if (fast.isArray(html)) { buffer.push(html.join('')); } else { buffer.push(html); } } // @private me.html = buffer.join(''); if (me.compiled) { me.compile(); } }, isTemplate: true, disableFormats: false, re: /\{([\w\-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g, ___apply: function(values) { var me = this, useFormat = me.disableFormats !== true, fm = fast.Format, tpl = me, ret; if (me.compiled) { return me.compiled(values).join(''); } function fn(m, name, format, args) { if (format && useFormat) { if (args) { args = [values[name]].concat(fast.functionFactory('return ['+ args +'];')()); } else { args = [values[name]]; } if (format.substr(0, 5) == "this.") { return tpl[format.substr(5)].apply(tpl, args); } else { return fm[format].apply(fm, args); } } else { return values[name] !== undefined ? values[name] : ""; } } ret = me.html.replace(me.re, fn); //ret = me.compile(ret); //console.log(ret); return ret; }, /** * Appends the result of this template to the provided output array. * @param {Object/Array} values The template values. See {@link #apply}. * @param {Array} out The array to which output is pushed. * @return {Array} The given out array. */ ___applyOut: function(values, out) { var me = this; if (me.compiled) { out.push.apply(out, me.compiled(values)); } else { out.push(me.apply(values)); } return out; }, apply: function(values) { return this.applyOut(values, []).join(''); }, applyOut: function(values, out) { var me = this; if (!me.fn) { me.fn = me.compile(me.html); } //console.log(me.fn); //console.log(values); out = me.fn(values); //这里玩的很精妙,以后有时间再分析一下 //console.log(me.fn); //try { // me.fn.call(me, out, values, {}, 1, 1); //} catch (e) {} //console.log(out); return out; }, /** * @method applyTemplate * @member Ext.Template * Alias for {@link #apply}. * @inheritdoc Ext.Template#apply */ applyTemplate: function () { return this.apply.apply(this, arguments); }, /** * Sets the HTML used as the template and optionally compiles it. * @param {String} html * @param {Boolean} compile (optional) True to compile the template. * @return {Ext.Template} this */ set: function(html, compile) { var me = this; me.html = html; me.compiled = null; return compile ? me.compile() : me; }, compileARe: /\\/g, compileBRe: /(\r\n|\n)/g, compileCRe: /'/g, /** * Applies the supplied values to the template and inserts the new node(s) as the first child of el. * * @param {String/HTMLElement/Ext.Element} el The context element * @param {Object/Array} values The template values. See {@link #applyTemplate} for details. * @param {Boolean} returnElement (optional) true to return a Ext.Element. * @return {HTMLElement/Ext.Element} The new node or Element */ insertFirst: function(el, values, returnElement) { return this.doInsert('afterBegin', el, values, returnElement); }, /** * Applies the supplied values to the template and inserts the new node(s) before el. * * @param {String/HTMLElement/Ext.Element} el The context element * @param {Object/Array} values The template values. See {@link #applyTemplate} for details. * @param {Boolean} returnElement (optional) true to return a Ext.Element. * @return {HTMLElement/Ext.Element} The new node or Element */ insertBefore: function(el, values, returnElement) { return this.doInsert('beforeBegin', el, values, returnElement); }, /** * Applies the supplied values to the template and inserts the new node(s) after el. * * @param {String/HTMLElement/Ext.Element} el The context element * @param {Object/Array} values The template values. See {@link #applyTemplate} for details. * @param {Boolean} returnElement (optional) true to return a Ext.Element. * @return {HTMLElement/Ext.Element} The new node or Element */ insertAfter: function(el, values, returnElement) { return this.doInsert('afterEnd', el, values, returnElement); }, /** * Applies the supplied `values` to the template and appends the new node(s) to the specified `el`. * * For example usage see {@link Ext.Template Ext.Template class docs}. * * @param {String/HTMLElement/Ext.Element} el The context element * @param {Object/Array} values The template values. See {@link #applyTemplate} for details. * @param {Boolean} returnElement (optional) true to return an Ext.Element. * @return {HTMLElement/Ext.Element} The new node or Element */ append: function(el, values, returnElement) { return this.doInsert('beforeEnd', el, values, returnElement); }, doInsert: function(where, el, values, returnEl) { el = fast.id(el); //var newNode = Ext.DomHelper.insertHtml(where, el, this.apply(values)); //return returnEl ? Ext.get(newNode, true) : newNode; }, /** * Applies the supplied values to the template and overwrites the content of el with the new node(s). * * @param {String/HTMLElement/Ext.Element} el The context element * @param {Object/Array} values The template values. See {@link #applyTemplate} for details. * @param {Boolean} returnElement (optional) true to return a Ext.Element. * @return {HTMLElement/Ext.Element} The new node or Element */ overwrite: function(el, values, returnElement) { el = fast.id(el); fast.html(el,this.apply(values)); return el.firstChild; }, ua : navigator.userAgent.toLowerCase(), ie : /msie(\d+\.\d+)/i.test(this.ua) ? (document.documentMode || (+RegExp['\x241'])) : undefined, useEval: /gecko/i.test(this.ua) && !/like gecko/i.test(this.ua), // See http://jsperf.com/nige-array-append for quickest way to append to an array of unknown length // (Due to arbitrary code execution inside a template, we cannot easily track the length in var) // On IE6 and 7 myArray[myArray.length]='foo' is better. On other browsers myArray.push('foo') is better. useIndex: this.ie && this.ie < 8, useFormat: true, propNameRe: /^[\w\d\$]*$/, compile: function (tpl) { var me = this,tpl = tpl || me.html, code = me.generate(tpl); //console.log(tpl); //debugger; //console.log(code); return me.useEval ? me.evalTpl(code) : (new Function('window', code))(window); }, generate: function (tpl) { var me = this; //console.log("me",me.fnArgs); me.body = [ 'var c0=values, a0 = fast.isArray(c0), p0=parent, n0=xcount || 1, i0=1, out=[], v;\n' ]; me.funcs = [ // note: Ext here is properly sandboxed 'var fm=fast.Format;' ]; me.switches = []; me.parse(tpl); !me.fnArgs && (me.fnArgs = "values"); me.funcs.push( (me.useEval ? '$=' : 'return') + ' function (' + me.fnArgs + ') {', me.body.join(''), 'return out;}' ); var code = me.funcs.join('\n'); return code; }, //----------------------------------- // XTemplateParser callouts doText: function (text) { var me = this, out = me.body; text = text.replace(me.aposRe, "\\'").replace(me.newLineRe, '\\n'); if (me.useIndex) { out.push('out[out.length]=\'', text, '\'\n'); } else { out.push('out.push(\'', text, '\')\n'); } }, doExpr: function (expr) { var out = this.body; expr = expr.replace("values","vvv"); out.push('if ((v=' + expr + ')!==undefined) out'); if (this.useIndex) { out.push('[out.length]=String(v)\n'); } else { out.push('.push(String(v))\n'); } }, doTag: function (tag) { this.doExpr(this.parseTag(tag)); }, doElse: function () { this.body.push('} else {\n'); }, doEval: function (text) { this.body.push(text, '\n'); }, doIf: function (action, actions) { var me = this; // If it's just a propName, use it directly in the if if (me.propNameRe.test(action)) { me.body.push('if (', me.parseTag(action), ') {\n'); } // Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values) else { me.body.push('if (', me.addFn(action), me.callFn, ') {\n'); } if (actions.exec) { me.doExec(actions.exec); } }, doElseIf: function (action, actions) { var me = this; // If it's just a propName, use it directly in the else if if (me.propNameRe.test(action)) { me.body.push('} else if (', me.parseTag(action), ') {\n'); } // Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values) else { me.body.push('} else if (', me.addFn(action), me.callFn, ') {\n'); } if (actions.exec) { me.doExec(actions.exec); } }, doSwitch: function (action) { var me = this; // If it's just a propName, use it directly in the switch if (me.propNameRe.test(action)) { me.body.push('switch (', me.parseTag(action), ') {\n'); } // Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values) else { me.body.push('switch (', me.addFn(action), me.callFn, ') {\n'); } me.switches.push(0); }, doCase: function (action) { var me = this, cases = Ext.isArray(action) ? action : [action], n = me.switches.length - 1, match, i; if (me.switches ) { me.body.push('break;\n'); } else { me.switches ++; } for (i = 0, n = cases.length; i < n; ++i) { match = me.intRe.exec(cases[i]); cases[i] = match ? match[1] : ("'" + cases[i].replace(me.aposRe,"\\'") + "'"); } me.body.push('case ', cases.join(': case '), ':\n'); }, doDefault: function () { var me = this, n = me.switches.length - 1; if (me.switches ) { me.body.push('break;\n'); } else { me.switches ++; } me.body.push('default:\n'); }, doEnd: function (type, actions) { var me = this, L = me.level-1; if (type == 'for') { /* To exit a for loop we must restore the outer loop's context. The code looks like this (which goes with that produced by doFor: for (...) { // the part generated by doFor ... // the body of the for loop // ... any tpl for exec statement goes here... } parent = p1; values = r2; xcount = n1; xindex = i1 */ if (actions.exec) { me.doExec(actions.exec); } me.body.push('}\n'); me.body.push('parent=p',L,';values=r',L+1,';xcount=n',L,';xindex=i',L,'\n'); } else if (type == 'if' || type == 'switch') { me.body.push('}\n'); } }, doFor: function (action, actions) { var me = this, s = me.addFn(action), L = me.level, up = L-1; me.body.push('var c',L,'=',s,me.callFn,', a',L,'=fast.isArray(c',L,'), p',L,'=c',up,',r',L,'=values\n', 'parent=a',up,'?c',up,'[i',up,']:p',L,'\n', //'for (var i',L,'=0,n',L,'=a',L,'?c',L,'.length:(c',L,'?1:0), xcount=n',L,';i',L,'<n'+L+';++i',L,'){\n', 'for (var i0 = 0,i1=0, l0 = values.length,xcount=l0; i0 < l0; i0 += 1){\n', 'vvv=values[i0]\n', 'xindex=i',L,'+1\n'); }, doExec: function (action, actions) { var me = this, name = 'f' + me.funcs.length; me.funcs.push('function ' + name + '(' + me.fnArgs + ') {', ' try { with(values) {', ' ' + action, ' }} catch(e) {}', '}'); me.body.push(name + me.callFn + '\n'); }, //----------------------------------- // Internal addFn: function (body) { var me = this, name = 'f' + me.funcs.length; !me.fnArgs && (me.fnArgs = "values"); if (body === '.') { me.funcs.push('function ' + name + '(' + me.fnArgs + ') {', ' return values', '}'); } else if (body === '..') { me.funcs.push('function ' + name + '(' + me.fnArgs + ') {', ' return parent', '}'); } else { me.funcs.push('function ' + name + '(' + me.fnArgs + ') {', ' try { with(values) {', ' return(' + body + ')', ' }} catch(e) {}', '}'); } return name; }, parseTag: function (tag) { var m = this.tagRe.exec(tag), name = m[1], format = m[2], args = m[3], math = m[4], v; //console.log(m); // name = "." - Just use the values object. if (name == '.') { // filter to not include arrays/objects/nulls v = 'fast.inArray(["string", "number", "boolean"], typeof values) > -1 || fast.isDate(values) ? values : ""'; } // name = "#" - Use the xindex else if (name == '#') { v = 'xindex'; } else if (name.substr(0, 7) == "parent.") { v = name; } // compound Javascript property name (e.g., "foo.bar") else if (isNaN(name) && name.indexOf('-') == -1 && name.indexOf('.') != -1) { v = "values." + name; } // number or a '-' in it or a single word (maybe a keyword): use array notation // (http://jsperf.com/string-property-access/4) else { v = "values['" + name + "']"; } if (math) { v = '(' + v + math + ')'; } //console.log(v); if (format && this.useFormat) { args = args ? ',' + args : ""; if (format.substr(0, 5) != "this.") { format = "fm." + format + '('; } else { format += '('; } } else { return v; } return format + v + args + ')'; }, // @private evalTpl: function ($) { // We have to use eval to realize the code block and capture the inner func we also // don't want a deep scope chain. We only do this in Firefox and it is also unhappy // with eval containing a return statement, so instead we assign to "$" and return // that. Because we use "eval", we are automatically sandboxed properly. eval($); return $; }, newLineRe: /\r\n|\r|\n/g, aposRe: /[']/g, intRe: /^\s*(\d+)\s*$/, tagRe: /([\w-\.\#]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\/]\s?[\d\.\+\-\*\/\(\)]+)?/, doTpl: function(){}, parse: function (str) { //str = this.html; var me = this, len = str.length, aliases = { elseif: 'elif' }, topRe = me.topRe, actionsRe = me.actionsRe, index, stack, s, m, t, prev, frame, subMatch, begin, end, actions; me.level = 0; me.stack = stack = []; for (index = 0; index < len; index = end) { topRe.lastIndex = index; m = topRe.exec(str); //console.log(m); if (!m) { me.doText(str.substring(index, len)); break; } begin = m.index; end = topRe.lastIndex; if (index < begin) { me.doText(str.substring(index, begin)); } if (m[1]) { end = str.indexOf('%}', begin+2); me.doEval(str.substring(begin+2, end)); end += 2; } else if (m[2]) { end = str.indexOf(']}', begin+2); me.doExpr(str.substring(begin+2, end)); end += 2; } else if (m[3]) { // if ('{' token) me.doTag(m[3]); } else if (m[4]) { // content of a <tpl xxxxxx> tag actions = null; while ((subMatch = actionsRe.exec(m[4])) !== null) { s = subMatch[2] || subMatch[3]; if (s) { s = fast.decodeHTML(s); // decode attr value t = subMatch[1]; t = aliases[t] || t; actions = actions || {}; prev = actions[t]; if (typeof prev == 'string') { actions[t] = [prev, s]; } else if (prev) { actions[t].push(s); } else { actions[t] = s; } } } if (!actions) { if (me.elseRe.test(m[4])) { me.doElse(); } else if (me.defaultRe.test(m[4])) { me.doDefault(); } else { me.doTpl(); stack.push({ type: 'tpl' }); } } else if (actions['if']) { me.doIf(actions['if'], actions) stack.push({ type: 'if' }); } else if (actions['switch']) { me.doSwitch(actions['switch'], actions) stack.push({ type: 'switch' }); } else if (actions['case']) { me.doCase(actions['case'], actions); } else if (actions['elif']) { me.doElseIf(actions['elif'], actions); } else if (actions['for']) { ++me.level; me.doFor(actions['for'], actions); stack.push({ type: 'for', actions: actions }); } else if (actions.exec) { me.doExec(actions.exec, actions); stack.push({ type: 'exec', actions: actions }); } /* else { // todo - error } /**/ } else { frame = stack.pop(); //console.log(frame); frame && me.doEnd(frame.type, frame.actions); if (frame && frame.type == 'for') { --me.level; } } } }, // Internal regexes topRe: /(?:(\{\%)|(\{\[)|\{([^{}]*)\})|(?:<tpl([^>]*)\>)|(?:<\/tpl>)/g, actionsRe: /\s*(elif|elseif|if|for|exec|switch|case|eval)\s*\=\s*(?:(?:["]([^"]*)["])|(?:[']([^']*)[']))\s*/g, defaultRe: /^\s*default\s*$/, elseRe: /^\s*else\s*$/ };
调用方式:
<div id="exttpl" style="height:100px;overflow-y: auto"></div> <script> var etpl = ['<table width="100%" border=1>', '<tpl for=".">', '<tr>', '<td>{name}</td>', '<td>{sex}</td>', '<td>{age}</td>','<td>{date}</td>','<td>{uid}</td>', '</tr>', '</tpl>', '</table>']; extpl.constructor(etpl); extpl.overwrite("exttpl",data); </script>
ext模版分两种,一种是templete,数据模型只处理字符串和数组,另外一种是xtemplete,可以处理对象,上面剥离的是xtemplete。在剥离的过程不禁惊叹js的精髓,原来js可以这样写!ext的大神们很精巧的拼装了一个内置函数,核心函数在generate和parse,generate负责组装,parse负责解析。
然后测试了一下,速度更是惊人,几乎和测试字符串模式(tp函数)跑平!那么多的判断分支,神了,再膜拜一下ext。
细嚼完ext,于是又回头看了一下jquery,由于时间问题没来得及剥离,粗略的写了一个用例。
<div style="height:100px;overflow-y: auto"> <table width="100%" border=1 id="jqtpl"></table> </div> <script id='templateName' type='text/x-jquery-tmpl'> <tr><td>${name}</td><td>${sex}</td><td>${age}</td><td>${date}</td><td>${uid}</td></tr> </script> <script type="text/javascript"> $('#templateName').tmpl(data).appendTo('#jqtpl'); </script>
测试中,jquery的用时在最长的,因为没有剥离出内核,所以不能妄加评论。但是它的写法是最精简的,值得学习和借鉴。
全部测试数据如下(单位:秒):
chrome:
测试对象的模式:用时0.04700016975402832
测试字符串模式:用时0.03299999237060547
测试extTmplete模式:用时0.03299999237060547
测试jquerytmpl模式:用时0.11500000953674316
ie9:
测试对象的模式:用时0.44099998474121093
测试字符串模式:用时0.03399991989135742
测试extTmplete模式:用时0.032000064849853516
测试jquerytmpl模式:用时0.3899998664855957
走了一圈之后再回顾自己写的模版,发现了自己的很多不足,急于结果的实现,对过程的把控没做合理的布局,实现上还需要做进一步推敲。
总结:
优秀js模版几个关键因素:
一、支持多级数据,无论ext还是jquery都支持。比如data数据,模版内可以做data.param1循环也可以做data.param2循环。
二、支持模版助手helper,可以通过助手任意处理模版里的控件,给模版提供灵活性。
三、有完善的容错机制。
四、支持内嵌循环。
五、易用性和速度效率,jquery的模版为什么会使用那么广是有原因的,用户能不能接受才是最关键的。
相关文章推荐
- js模版深度解析
- C++模版深度解析
- js模版解析
- C++模版深度解析
- C++模版深度解析
- 深度解析C++模版
- vue-cli的webpack模版,相关配置文件dev-server.js与webpack.config.js配置解析
- JS中parseInt()、Numer()深度解析
- js 对有“命名空间”的表单做深度解析
- 微信小程序全方位深度解析课程Dome-First项目app.js
- js 对有“命名空间”的表单做深度解析
- C++模版深度解析
- JS模版解析
- 深度解析C++模版
- JS作用域深度解析
- vue-cli的webpack模版项目配置解析-build/dev-server.js
- Js简介3——各种方法的解析
- 前端JS模版库kino.razor - 原理流程分析 - 改进版轮子RazorJs
- 最简单的JS模版引擎
- linux ssh 使用深度解析(key登录详解)