javascript模版引擎-tmpl的bug修复与性能优化分析


Posted in Javascript onOctober 23, 2011

精妙的 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('<%=name%>//<%=id%> ', {name:'糖饼', id: '1987'});

它就会报错。若正常工作,它应该输出:糖饼/1987
实际上解决起来很简单,添加一行正则对转义符进行转义:
str.replace(/\\/g, "\\\\")

二、它有时候无法正确区分第一个参数是ID还是模板。
假若页面模板ID带有下划线,如 tmpl-photo-thumb 它不会去查找这个名称的模板,会认为这传入的是原始模板直接编译输出。
原始模板与元素id最直观的区别就是是否含有空格,因此改动下正则表达式即可:
view sourceprint?1 !/\s/.test(str)
三、它内部还残有一处测试用的代码,可删除。
print=function(){p.push.apply(p,arguments);}

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的参数名(即变量名),然后使用函数的appley调用方式传给那些参数。
tmpl 则使用了javascript不常用的 with 语句实现。 实现方式很简洁,省去了var这个关键字。
tmpl 性能问题就出在 with 上面。javascript 提供的 with 语句,本意是想用来更快捷的访问对象的属性。不幸的是,with语句在语言中的存在,就严重影响了 javascript 引擎的速度,因为它阻止了变量名的词法作用域绑定。
优化 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 默认对嵌入到页面中的模板进行了缓存优化(即第一个参数为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 ? 湖南-长沙
Javascript 相关文章推荐
Javascript this指针
Jul 30 Javascript
javascript实现的距离现在多长时间后的一个格式化的日期
Oct 29 Javascript
基于jquery循环map功能的代码
Feb 26 Javascript
Jquery 点击按钮显示和隐藏层的代码
Jul 25 Javascript
JS Replace 全部替换字符的用法小结
Dec 24 Javascript
11种ASP连接数据库的方法
Sep 18 Javascript
javascript基本数据类型和转换
Mar 17 Javascript
JavaScript定义及输出螺旋矩阵的方法详解
Dec 01 Javascript
bootstrap 点击空白处popover弹出框隐藏实例
Jan 24 Javascript
使用gulp构建前端自动化的方法示例
Dec 25 Javascript
JS实现容器模块左右拖动效果
Jan 14 Javascript
区分vue-router的hash和history模式
Oct 03 Javascript
js面向对象设计用{}好还是function(){}好(构造函数)
Oct 23 #Javascript
jQuery EasyUI API 中文文档 - TimeSpinner时间微调器
Oct 23 #Javascript
利用jQuery插件扩展识别浏览器内核与外壳的类型和版本的实现代码
Oct 22 #Javascript
js两行代码按指定格式输出日期时间
Oct 21 #Javascript
jQuery中live方法的重复绑定说明
Oct 21 #Javascript
jquery(live)中File input的change方法只起一次作用的解决办法
Oct 21 #Javascript
jQuery EasyUI API 中文文档 - NumberSpinner数值微调器使用介绍
Oct 21 #Javascript
You might like
PHP 时间日期操作实战
2011/08/26 PHP
php通过session防url攻击方法
2014/12/10 PHP
CL vs ForZe BO5 第一场 2.13
2021/03/10 DOTA
JS控制图片等比例缩放的示例代码
2013/12/24 Javascript
jquery中push()的用法(数组添加元素)
2014/11/25 Javascript
基于js实现投票的实例代码
2015/08/04 Javascript
jQuery+CSS3实现3D立方体旋转效果
2015/11/10 Javascript
JavaScript程序开发之JS代码放置的位置
2016/01/15 Javascript
js事件处理程序跨浏览器解决方案
2016/03/27 Javascript
使用vue.js开发时一些注意事项
2016/04/27 Javascript
数组Array的一些方法(总结)
2017/02/17 Javascript
jquery拼接ajax 的json和字符串拼接的方法
2017/03/11 Javascript
微信小程序实战之自定义toast(6)
2017/04/18 Javascript
vue3.0 CLI - 3.2 路由的初级使用教程
2018/09/20 Javascript
基于JavaScript伪随机正态分布代码实例
2019/11/07 Javascript
vue之debounce属性被移除及处理详解
2019/11/13 Javascript
js判断在哪个浏览器打开项目的方法
2020/01/21 Javascript
js实现飞机大战小游戏
2020/08/26 Javascript
[01:33]完美世界DOTA2联赛PWL S3 集锦第二期
2020/12/21 DOTA
日常整理python执行系统命令的常见方法(全)
2015/10/22 Python
Python简单实现子网掩码转换的方法
2016/04/13 Python
python分治法求二维数组局部峰值方法
2018/04/03 Python
python 实现在Excel末尾增加新行
2018/05/02 Python
利用Python实现在同一网络中的本地文件共享方法
2018/06/04 Python
python实现QQ空间自动点赞功能
2019/04/09 Python
在pyqt5中QLineEdit里面的内容回车发送的实例
2019/06/21 Python
python代码实现逻辑回归logistic原理
2019/08/07 Python
FitFlop澳大利亚官网:英国符合人体工学的鞋类品牌
2017/06/05 全球购物
国外软件测试工程师面试题
2016/12/09 面试题
如何拷贝一整个Java对象,包括它的状态
2013/12/27 面试题
财会自我鉴定范文
2013/12/27 职场文书
2015年全国保险公众宣传日活动方案
2015/05/06 职场文书
再次探讨go实现无限 buffer 的 channel方法
2021/06/13 Golang
改造DE1103三步曲
2022/04/07 无线电
Java 关于String字符串原理上的问题
2022/04/07 Java/Android
Elasticsearch6.2服务器升配后的bug(避坑指南)
2022/09/23 Servers