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 定义function的三种方式小结
Oct 16 Javascript
JavaScript 解析Json字符串的性能比较分析代码
Dec 16 Javascript
TimergliderJS 一个基于jQuery的时间轴插件
Dec 07 Javascript
javascript常用对话框小集
Sep 13 Javascript
seajs加载jquery时提示$ is not a function该怎么解决
Oct 23 Javascript
Vue数据驱动模拟实现4
Jan 12 Javascript
JavaScript仿微信(电话)联系人列表滑动字母索引实例讲解(推荐)
Aug 16 Javascript
详解vue中引入stylus及报错解决方法
Sep 22 Javascript
深入理解Promise.all
Aug 08 Javascript
node.js制作一个简单的登录拦截器
Feb 10 Javascript
Js Snowflake(雪花算法)生成随机ID的实现方法
Aug 26 Javascript
解决vue scoped scss 无效的问题
Sep 04 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
php4的session功能评述(三)
2006/10/09 PHP
PHP实现二叉树的深度优先与广度优先遍历方法
2015/09/28 PHP
PHP简单实现上一页下一页功能示例
2016/09/14 PHP
PHP测试框架PHPUnit组织测试操作示例
2018/05/28 PHP
javascrip关于继承的小例子
2013/05/10 Javascript
javascript页面动态显示时间变化示例代码
2013/12/18 Javascript
jQuery实现友好的轮播图片特效
2015/01/12 Javascript
微信小程序 MINA文件结构
2016/10/17 Javascript
Bootstrap整体框架之JavaScript插件架构
2016/12/15 Javascript
JavaScript中数据类型转换总结
2016/12/25 Javascript
基于vue2.0动态组件及render详解
2018/03/17 Javascript
vue-cli 引入jQuery,Bootstrap,popper的方法
2018/09/03 jQuery
基于vue-upload-component封装一个图片上传组件的示例
2018/10/16 Javascript
javascript中innerHTML 获取或替换html内容的实现代码
2020/03/17 Javascript
如何在vue 中引入使用jquery
2020/11/10 jQuery
[01:19:54]DOTA2上海特级锦标赛主赛事日 - 2 败者组第二轮#1Alliance VS EHOME
2016/03/03 DOTA
[01:45]典藏宝瓶2+祈求者身心——这就是DOTA2TI9总奖金突破3000万美元的秘密
2019/07/21 DOTA
python网络编程实例简析
2014/09/26 Python
在Python中使用mechanize模块模拟浏览器功能
2015/05/05 Python
python使用 HTMLTestRunner.py生成测试报告
2017/10/20 Python
python中print()函数的“,”与java中System.out.print()函数中的“+”功能详解
2017/11/24 Python
使用python将请求的requests headers参数格式化方法
2019/01/02 Python
python变量赋值方法(可变与不可变)
2019/01/12 Python
Python爬虫运用正则表达式的方法和优缺点
2019/08/25 Python
python字符串的index和find的区别详解
2020/06/20 Python
Clarins娇韵诗美国官网:法国天然护肤品牌
2016/09/26 全球购物
美国生鲜及杂货电商:FreshDirect
2018/01/29 全球购物
美国礼品卡交易网站:Cardpool
2018/08/27 全球购物
什么是java序列化,如何实现java序列化
2012/11/14 面试题
俄语专业毕业生推荐信
2013/10/28 职场文书
媒矿安全生产承诺书
2014/05/23 职场文书
群众路线班子对照检查材料
2014/09/25 职场文书
2014最新实习证明模板
2014/10/02 职场文书
中学生思想品德评语
2014/12/31 职场文书
2016年庆祝六一儿童节活动总结
2016/04/06 职场文书
Node与Python 双向通信的实现代码
2021/07/16 Javascript