在这篇文章中,我简单介绍了前端模板引擎。John Resig写的tmpl函数麻雀虽小五脏俱全,足以满足日常开发需要。本文主要探讨一下tmpl的性能优化。
先复习一下tmpl的源代码:
var tmpl = (function() {
var cache = { };
return function(str, data) {
var fn = cache[str];
if (!fn) {
fn = new Function("obj",
"var p=[];" +
"with(obj){p.push('" +
str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'")
+ "');}return p.join('');"
);
cache[str] = fn;
}
return fn(data);
};
})();
其实现原理是:模板内容(str变量)经过一系列字符串操作替换成原生的Javascript语句,再通过new Function创建为模板函数并缓存起来,把数据传入模板函数执行后,其返回值就是渲染结果。我们把从模板字符串到模板函数的转换过程称作模板编译。可见,由于缓存(cache对象)的存在,对同一份模板而言,编译只会发生一次。所以,改良模板函数的执行过程对性能提升帮助更大。为了加深理解,我们把fn打印出来看看:
var template =
'<ul>' +
'<% for (var i = 0; i < list.length; i++) { %>' +
'<li><a href="<%=list[i].url%>" target="_blank"><%=list[i].title%></a></li>' +
'<% } %>' +
'</ul>';
var list = [
{ url: 'http://www.baidu.com/', title: '百度' },
{ url: 'http://www.163.com/', title: '网易' },
{ url: 'http://www.weibo.com/', title: '微博' },
{ url: 'http://www.youku.com/', title: '优酷' },
{ url: 'http://www.cnbeta.com/', title: 'cnBeta' }
];
var tmpl = (function() {
var cache = { };
return function(str, data) {
var fn = cache[str];
if (!fn) {
fn = new Function('...此处省略...');
console.log(fn);
cache[str] = fn;
}
return fn(data);
};
})();
tmpl(template, { list: list });
// 打印结果
function anonymous(obj
/**/) {
var p=[];with(obj){p.push('<ul>'); for (var i = 0; i < list.length; i++) { p.push('<li><a href="',list[i].url,'" target="_blank">',list[i].title,'</a></li>'); } p.push('</ul>');}return p.join('');
}
这个函数主要有两个性能问题:
- 使用数组拼接字符串。在稍微新一点的浏览器中,原生字符串拼接比数组拼接性能更高。
- with语句是性能杀手。
第一个问题很好解决,把数组操作都改成字符串操作即可;第二个问题就比较麻烦,如果没有了with语句,如何让函数正常执行呢?
function anonymous() {
var p=[];p.push('<ul>'); for (var i = 0; i < list.length; i++) { p.push('<li><a href="',list[i].url,'" target="_blank">',list[i].title,'</a></li>'); } p.push('</ul>');return p.join('');
}
办法还是有的,只要能把list作为函数参数即可。代码如下(暂时不考虑缓存):
var tmpl_optimized = (function() {
function compile(str) {
return "var __result__='';" +
"__result__+='" +
str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "'+$1+'")
.split("\t").join("';")
.split("%>").join("__result__+='")
.split("\r").join("\\'")
+ "';return __result__";
}
return function(str, data) {
var keys = [ ], values = [ ];
for (var k in data) {
if (data.hasOwnProperty(k)) {
keys.push(k);
values.push(data[k]);
}
}
var fn = new Function(keys, compile(str));
console.log(fn);
return fn.apply(this, values);
};
});
tmpl_optimized(template, { list: list });
这段代码的关键点:
- 模板可能会用到data的所有属性,所以要进行遍历提取所有属性名(keys)和值(values);
- keys数组即为模板函数的参数列表;
- values的顺序与keys相对应,所以fn.apply(this, values)就可以依次把参数传进去。
此时打印出来的fn为:
function anonymous(list
/**/) {
var __result__='';__result__+='<ul>'; for (var i = 0; i < list.length; i++) { __result__+='<li><a href="'+list[i].url+'" target="_blank">'+list[i].title+'</a></li>'; } __result__+='</ul>';return __result__
}
恰好达到前面说的目的。小试牛刀已经成功,下面就要研究如何加上缓存机制了。tmpl是使用模板本身作为缓存标识的,但是在tmpl_optimized中就不能只这么干了,因为对同一份模板来说,不同的数据可能产生不同的模板函数,例如:
- 当data为 { a: 1, b: 2 } 时,模板函数为 function(a, b) { ... } ;
- 当data为 { c: 1, d: 2 } 时,模板函数为 function(c, d) { ... } 。
也就是说,data对象内属性的差异会产生不同的模板函数,所以这里需要两层缓存,第一层用于记录compile函数的返回结果,第二层用于记录特定属性组合下的模板函数:
var tmpl_optimized = (function() {
var cache = { };
function compile(str) {
return "var __result__='';" +
"__result__+='" +
str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "'+$1+'")
.split("\t").join("';")
.split("%>").join("__result__+='")
.split("\r").join("\\'")
+ "';return __result__";
}
return function(str, data) {
// 第一层缓存,使用模板作为缓存标识
var tplObj = cache[str];
if (!tplObj) {
tplObj = cache[str] = {
fnBody: compile(str),
fnCache: { }
};
}
var keys = [ ], values = [ ];
for (var k in data) {
if (data.hasOwnProperty(k)) {
keys.push(k);
values.push(data[k]);
}
}
// 第二层缓存,使用属性名作为缓存标识
var cacheKey = keys.toString(), fn = tplObj.fnCache[cacheKey];
if (!fn) {
fn = tplObj.fnCache[cacheKey] = new Function(keys, tplObj.fnBody);
}
return fn.apply(this, values);
};
})();
最后可以通过这段代码来对比tmpl和tmpl_optimized执行时间上的差异:
console.time('original');
for (var i = 0; i < 10; i++) {
tmpl(template, { list: list });
}
console.timeEnd('original');
console.time('optimized');
for (var i = 0; i < 10; i++) {
tmpl_optimized(template, { list: list });
}
console.timeEnd('optimized');
由于不同机器有不同的性能,所以这里就不详细贴数据了。此外,性能优化的手段并不是永远都有效的,可能将来有一天JS引擎对with语句进行大幅度优化后,tmpl会比tmpl_optimized更高效。
评论 (1条)