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模拟原生态App上拉刷新下拉加载更多页面及原理
Aug 10 Javascript
点击按钮出现60秒倒计时的简单js代码(推荐)
Jun 07 Javascript
利用Angularjs和bootstrap实现购物车功能
Aug 31 Javascript
js 点击a标签 获取a的自定义属性方法
Nov 21 Javascript
Angular下H5上传图片的方法(可多张上传)
Jan 09 Javascript
在React 组件中使用Echarts的示例代码
Nov 08 Javascript
js+canvas实现滑动拼图验证码功能
Mar 26 Javascript
微信小程序内拖动图片实现移动、放大、旋转的方法
Sep 04 Javascript
Jquery的autocomplete插件用法及参数讲解
Mar 12 jQuery
jQuery实现的上拉刷新功能组件示例
May 01 jQuery
Vue+element-ui添加自定义右键菜单的方法示例
Dec 08 Vue.js
jQuery实现简单弹幕制作
Dec 10 jQuery
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中的session永不过期的解决思路及实现方法分享
2011/04/20 PHP
php控制linux服务器常用功能 关机 重启 开新站点等
2012/09/05 PHP
使用tp框架和SQL语句查询数据表中的某字段包含某值
2019/10/18 PHP
JS IE和FF兼容性问题汇总
2009/02/09 Javascript
jQuery中使用了document和window哪些属性和方法小结
2011/09/13 Javascript
Jquery 模拟用户点击超链接或者按钮的方法
2013/10/25 Javascript
JavaScript不使用prototype和new实现继承机制
2014/12/29 Javascript
JS基于FileSystemObject创建一个指定路径的TXT文本文件
2015/08/05 Javascript
Jquery实现弹性滑块滑动选择数值插件
2015/08/08 Javascript
JavaScript编程学习技巧汇总
2016/02/21 Javascript
JavaScript中获取时间的函数集
2016/08/16 Javascript
jquery 动态合并单元格的实现方法
2016/08/26 Javascript
JS原型链 详解及示例代码
2016/09/06 Javascript
Angular.js之作用域scope'@','=','&amp;'实例详解
2017/02/28 Javascript
ES6字符串模板,剩余参数,默认参数功能与用法示例
2017/04/06 Javascript
Node.JS更改Windows注册表Regedit的方法小结
2017/08/18 Javascript
jQuery中复合选择器简单用法示例
2018/03/31 jQuery
浅析JavaScript异步代码优化
2019/03/18 Javascript
微信小程序实现拍照画布指定区域生成图片
2019/07/18 Javascript
javascript实现倒计时效果
2020/02/17 Javascript
Nodejs实现WebSocket代码实例
2020/05/19 NodeJs
JavaScript中的执行环境和作用域链
2020/09/04 Javascript
在Python中处理字符串之ljust()方法的使用简介
2015/05/19 Python
Python 遍历子文件和所有子文件夹的代码实例
2016/12/21 Python
深入学习python多线程与GIL
2019/08/26 Python
wxpython绘制圆角窗体
2019/11/18 Python
nginx搭建基于python的web环境的实现步骤
2020/01/03 Python
TensorFlow实现从txt文件读取数据
2020/02/05 Python
Python 去除字符串中指定字符串
2020/03/05 Python
HTML5+CSS3实例 :canvas 模拟实现电子彩票刮刮乐代码
2016/12/30 HTML / CSS
Canvas中设置width与height的问题浅析
2018/11/01 HTML / CSS
澳大利亚汽车零部件、音响及配件超市:Automotive Superstore
2018/06/19 全球购物
ColourPop美国官网:卡拉泡泡,洛杉矶彩妆品牌
2019/04/28 全球购物
医护人员英文求职信范文
2013/11/26 职场文书
python tkinter模块的简单使用
2021/04/07 Python
Python机器学习之决策树和随机森林
2021/07/15 Javascript