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 相关文章推荐
自定义一个jquery插件[鼠标悬浮时候 出现说明label]
Jun 27 Javascript
THREE.JS入门教程(3)着色器-下
Jan 24 Javascript
JS 两日期相减,获得天数的小例子(兼容IE,FF)
Jul 01 Javascript
HTML,CSS,JavaScript速查表推荐
Dec 02 Javascript
node.js中的fs.lchmodSync方法使用说明
Dec 16 Javascript
JavaScript DOM事件(笔记)
Apr 08 Javascript
JSON对象 详解及实例代码
Oct 18 Javascript
老生常谈原生JS执行环境与作用域
Nov 22 Javascript
微信小程序开发(二)图片上传+服务端接收详解
Jan 11 Javascript
简单了解vue中的v-if和v-show的区别
Oct 08 Javascript
Vue ElementUI实现:限制输入框只能输入正整数的问题
Jul 31 Javascript
利用js实现简易红绿灯
Oct 15 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
浅析Dos下运行php.exe,出现没有找到php_mbstring.dll 错误的解决方法
2013/06/29 PHP
检查用户名是否已在mysql中存在的php写法
2014/01/20 PHP
php删除数组元素示例分享
2014/02/17 PHP
非常好用的Zend Framework分页类
2014/06/25 PHP
php使用ftp实现文件上传与下载功能
2017/07/21 PHP
php实现支持中文的文件下载功能示例
2017/08/30 PHP
JavaScript 类的定义和引用 JavaScript高级培训 自定义对象
2010/04/27 Javascript
在linux中使用包管理器安装node.js
2015/03/13 Javascript
javascript删除元素节点removeChild()用法实例
2015/05/26 Javascript
Bootstrap表单组件教程详解
2016/04/26 Javascript
基于jQuery实现选项卡效果
2017/01/04 Javascript
基于JS实现移动端左滑删除功能
2017/07/28 Javascript
jquery实现倒计时小应用
2017/09/19 jQuery
angularjs数组判断是否含有某个元素的实例
2018/02/27 Javascript
NodeJs搭建本地服务器之使用手机访问的实例讲解
2018/05/12 NodeJs
webpack+vue-cil中proxyTable处理跨域的方法
2018/07/20 Javascript
关于自定义Egg.js的请求级别日志详解
2018/12/12 Javascript
js设计模式之代理模式及订阅发布模式实例详解
2019/08/15 Javascript
javascript实现简易计算器功能
2020/09/23 Javascript
vue+vant实现购物车全选和反选功能
2020/11/17 Vue.js
[43:47]完美世界DOTA2联赛PWL S3 LBZS vs Phoenix 第一场 12.09
2020/12/11 DOTA
python实现RSA加密(解密)算法
2016/02/17 Python
python距离测量的方法
2018/03/06 Python
Python 实现选择排序的算法步骤
2018/04/22 Python
python制作简单五子棋游戏
2019/06/18 Python
python中metaclass原理与用法详解
2019/06/25 Python
Pycharm使用之设置代码字体大小和颜色主题的教程
2019/07/12 Python
OpenCV+python实现实时目标检测功能
2020/06/24 Python
python 利用matplotlib在3D空间中绘制平面的案例
2021/02/06 Python
英语专业学生的自我评价
2013/12/30 职场文书
党员大会主持词
2014/04/02 职场文书
《穷人》教学反思
2014/04/08 职场文书
2014五一国际劳动节活动总结范文
2014/04/14 职场文书
员工廉洁自律承诺书
2014/05/26 职场文书
Golang全局变量加锁的问题解决
2021/05/08 Golang
Li list-style-image 图片垂直居中实现方法
2023/05/21 HTML / CSS