javascript模版引擎-tmpl的bug修复与性能优化分析
本文深入分析John Resig开发的javascript micro templating库,指出其存在的转义字符处理错误、ID识别缺陷及with语句导致的性能瓶颈。通过修复正则逻辑并移除with语句,采用传统传参方式,实测在Firefox等浏览器中性能提升显著,最终达到与YayaTemplate相当的水平。
精妙的 tmpl
精妙的 tmpl
前端领域存在众多开源模板库,但由 jQuery 作者 John Resig 开发的 “javascript micro templating” 方案尤为出色,仅用少量代码便实现了模板引擎的核心功能。
其具体介绍与使用指南可参考作者博客:http://ejohn.org/blog/javascript-micro-templating/
我们先来看一下其原始源码结构:
代码如下:
(function(){
var cache = {};
this.tmpl = function (str, data){
var fn = !/\W/.test(str) ?
cache[str] = cache[str] ||
tmpl(document.getElementById(str).innerHTML) :
new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments);};" +
"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('');");
return data ? fn( data ) : fn;
};
})(); 该函数虽短小却功能完备,包含数据注入、缓存机制及逻辑支持。若评选最节省资源的 JavaScript 自定义函数,$ 函数(document.getElementById 的简写版)位列第一,而 tmpl 紧随其后。
tmpl 美中不足
然而,在实际使用中我发现了一些不足之处:
tmpl 美中不足
一、无法正确处理转义字符,例如:
代码如下:
tmpl('<%=name%>//<%=id%> ', {name:'糖饼', id: '1987'}); 该调用会引发报错。若逻辑正常,预期输出应为:糖饼/1987
修复方法十分简单,只需添加一行正则表达式以处理转义符:
代码如下:
str.replace(/\\/g, "\\\\") 二、有时无法正确区分第一个参数是元素 ID 还是原始模板字符串。
若页面模板 ID 包含下划线,例如 tmpl-photo-thumb,它不会去查找该 ID 对应的模板,而是误认为传入的是原始模板并直接编译输出。
原始模板字符串与元素 ID 最直观的区别在于是否包含空格,因此只需修改正则表达式即可解决:
view sourceprint?1 !/\s/.test(str)
三、内部残留有一处用于测试的代码,建议删除。
代码如下:
print=function(){p.push.apply(p,arguments);} tmpl 效率的疑惑
tmpl 效率的疑惑
前阵子阅读百度 mux 介绍 YayaTemplate 的文章,原作者对各大流行模板引擎进行了效率测试,结论是 YayaTemplate 速度最快。尽管 tmpl 测试结果不如 YayaTemplate,但这消除我对性能的顾虑,实际应用中其性能与传统字符串拼接相差无几。只有在处理超大规模数据解析时,两者才会有显著的性能差距。(所谓超大规模?JavaScript 本身并不适合处理此类任务。若程序员一次性向浏览器插入上千条列表数据导致浏览器卡顿,无需怀疑:问题出在程序员身上,因为他未能妥善保护用户浏览器的性能。)
关于引擎效率排名,我认为这并非衡量模板引擎的首要标准,模板语法的易用性同样重要。此时 YayaTemplate 的模板语法显得较为晦涩,它为了节省几个正则表达式而在模板语法设计上走了捷径。
先展示 YayaTemplate 的源码:
代码如下:
//author:yaya,jihu
//uloveit.com.cn/template
//how to use? YayaTemplate("xxx").render({});
var YayaTemplate = YayaTemplate || function(str){
//核心分析方法
var _analyze=function(text){
return text.replace(/{\$(\s|\S)*?\$}/g,function(s){
return s.replace(/("|\\)/g,"\\$1")
.replace("{$",'_s.push("')
.replace("$}",'");')
.replace(/{\%([\s\S]*?)\%}/g, '",$1,"')
}).replace(/\r|\n/g,"");
};
//中间代码
var _temp = _analyze(document.getElementById(str)?document.getElementById(str).innerHTML:str);
//返回生成器render方法
return {
render : function(mapping){
var _a = [],_v = [],i;
for (i in mapping){
_a.push(i);
_v.push(mapping[i]);
}
return (new Function(_a,"var _s=[];"+_temp+" return _s;")).apply(null,_v).join("");
}
}
}; 若将性能问题上升到“学术”层面探讨,为什么 tmpl 会比 YayaTemplate 慢?
语法解析?尽管 YayaTemplate 使用了新颖的 JavaScript 包裹 HTML 的方式作为模板语法,但最终仍需通过正则表达式解析为标准 JavaScript 语法。在此环节,两者的正则效率差异不大,且双方都使用了缓存机制,确保原始模板仅解析一次。
数据转换?模板引擎最终将数据以变量形式保存在闭包中,以便模板获取。此处对比双方的变量声明机制:
YayaTemplate 使用传统传递参数的形式实现。它通过遍历数据对象,将对象的名值分离,然后将对象成员名称作为 new Function 的参数名(即变量名),最后使用函数的 apply 调用方式将这些参数传入。
tmpl 则使用了 JavaScript 中不常用的 with 语句实现。这种方式很简洁,省去了 var 关键字。
tmpl 的性能问题根源在于 with 语句。JavaScript 提供的 with 语句本意是更快捷地访问对象属性,不幸的是,with 语句在语言中的存在严重影响了 JavaScript 引擎的速度,因为它阻止了变量名的词法作用域绑定。
优化 tmpl
优化 tmpl
若 tmpl 去掉 with 语句,改用传统的传参方式,性能将大幅提升。经实测,在 24 万条数据下,Firefox 能提高 5 倍,Chrome 2.4 倍,Opera 1.84 倍,Safari 2.1 倍,IE6 1.1 倍,IE9 1.35 倍,最终性能与 YayaTemplate 不分上下。
测试地址:http://www.planeart.cn/demo/tmpl/tmpl.html
tmpl 优化版最终代码:
代码如下:
/**
* 微型模板引擎 tmpl 0.2
*
* 0.2 更新:
* 1. 修复转义字符与id判断的BUG
* 2. 放弃低效的 with 语句从而最高提升3.5倍的执行效率
* 3. 使用随机内部变量防止与模板变量产生冲突
*
* @author John Resig, Tang Bin
* @see http://ejohn.org/blog/javascript-micro-templating/
* @name tmpl
* @param {String} 模板内容或者装有模板内容的元素ID
* @param {Object} 附加的数据
* @return {String} 解析好的模板
*
* @example
* 方式一:在页面嵌入模板
* <script type="text/tmpl" id="tmpl-demo">
* <ol title="<%=name%>">
* <% for (var i = 0, l = list.length; i < length; i ++) { %>
* <li><%=list[i]%></li>
* <% } %>
* </ol>
* </script>
* tmpl('tmpl-demo', {name: 'demo data', list: [202, 96, 133, 134]})
*
* 方式二:直接传入模板:
* var demoTmpl =
* '<ol title="<%=name%>">'
* + '<% for (var i = 0, l = list.length; i < length; i ++) { %>'
* + '<li><%=list[i]%></li>'
* + '<% } %>'
* +'</ol>';
* var render = tmpl(demoTmpl);
* render({name: 'demo data', list: [202, 96, 133, 134]});
*
* 这两种方式区别在于第一个会自动缓存编译好的模板,
* 而第二种缓存交给外部对象控制,如例二中的 render 变量。
*/
var tmpl = (function (cache, $) {
return function (str, data) {
var fn = !/\s/.test(str)
? cache[str] = cache[str]
|| tmpl(document.getElementById(str).innerHTML)
: function (data) {
var i, variable = [$], value = [[]];
for (i in data) {
variable.push(i);
value.push(data[i]);
};
return (new Function(variable, fn.$))
.apply(data, value).join("");
};
fn.$ = fn.$ || $ + ".push('"
+ str.replace(/\\/g, "\\\\")
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join($ + ".push('")
.split("\r").join("\\'")
+ "');return " + $;
return data ? fn(data) : fn;
}})({}, '$' + (+ new Date)); 模板引擎依赖 Function 构造器实现,它与 eval 一样提供了使用文本访问 JavaScript 解析引擎的方法,这也会导致性能显著降低,但此时 JavaScript 中已别无他法。
使用 Function 构造器还会对参数名称有所限制,因此数据成员命名必须与 JavaScript 变量名规范保持一致,否则会报错。好在这个错误可以在运行时立即被发现,而不会成为一颗隐患地雷。
tmpl 使用小窍门
tmpl 使用小窍门
一、缓存优化。
tmpl 默认对嵌入到页面中的模板进行了缓存优化(即第一个参数为 ID 时),它仅对模板进行一次分析。若原始模板是直接传入到 tmpl 第一个参数中,且需要多次使用,建议用公用变量缓存起来,需要解析数据时再调用,以获得相同的优化效果。例如:
代码如下:
// 生成模板缓存
var render = tmpl(listTmpl);
// 可多次调用模板
elem.innerHTML = render(data1);
elem.innerHTML = render(data2);
... 二、避免未定义的变量引起系统崩溃。
若模板中定义了一个变量输出,而传入数据缺少该项目,就会出现变量未定义的错误,从而引起整个程序的崩溃。如果无法确保数据完整性,仍有方法对其成员进行探测。原版中暗含变量保存了原始传入的数据,即 obj;而在我的升级版本中则是关键字 this,例如:
代码如下:
<% if (this.dataName !== undefined) { %>
<%=dataName %>
<% } %> 三、调试模板。
由于模板引擎是用文本调用的 JavaScript 引擎,调试工具无法直接定位到出错的行。在升级版本中,你可以用调试工具输出编译好的模板缓存。例如调试这个模板:
代码如下:
<script id="tmpl" type="text/tmpl">
<ul>
<% for (var i = 0, l = list.length; i < l; i ++) { %>
<li><%=list[i].index%>. 用户: <%=list[i].user%>; 网站:<%=list[i].site%></li>
<% } %>
</ul> 输出缓存:
代码如下:
window.console(tmpl('tmpl').$); 日志结果:
代码如下:
"$1318348744541.push('
<ul> '); for (var i = 0, l = list.length; i < l; i ++) { $1318348744541.push('
<li>',list[i].index,'. 用户: ',list[i].user,'; 网站:',list[i].site,'</li>
'); } $1318348744541.push(' </ul>
');return $1318348744541" 现在你可以看到模板引擎编译好的 JavaScript 语句,可以对照这检查模板是否存在错误。($1318348744541 是一个随机名称的临时数组,可忽略)
最后非常感谢 tmpl 原作者与 YayaTemplate 作者的付出,正因为此我才有机会深入分析实现机制,解决问题并从中受益。独乐不如众乐,分享之。
唐斌 – 2011.10.09 – 湖南-长沙
配置 Matter.js 睡眠机制以优化静止物体性能
2026-11-05 15:43:12
JavaScript console.log高级用法:用table、group和time优化调试输出
2026-11-05 08:04:56
JS变量中存储HTML代码时的特殊字符转义方法
2026-11-04 19:04:09
JS怎么安全转换带千分位或货币符号的字符串为数字
2026-11-04 11:54:14
推荐几款稳定的在线JS反混淆工具,支持Obfuscator.io解密
2026-11-04 08:16:21
