您的位置:首页 > Web前端 > JavaScript

js模版深度解析

2012-09-04 16:37 288 查看
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);
}
}
};


另外创建了一个基类,这个基类后面的例子都会用到,希望读者注意。

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的大神们很精巧的拼装了一个内置函数,核心函数在generateparse,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的模版为什么会使用那么广是有原因的,用户能不能接受才是最关键的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: