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

精妙的 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 – 湖南-长沙

(0)

相关推荐

  • 基于jQuery的JavaScript模版引擎JsRender使用指南

    前言 JsRender是一款基于jQuery的JavaScript模版引擎,它具有如下特点: ·  简单直观 ·  功能强大 ·  可扩展的 ·  快如闪电 这些特性看起来很厉害,但几乎每个模版引擎,都会这么宣传... 由于工作需要,小菜才接触到此款模版引擎.使用了一段时间,发现它确实比较强大,但小菜觉得有些地方强大的过头了,反倒让人觉得很难理解. 另一方面,JsRender的官方文档比较详细,但其他资料出奇的少,遇到点什么问题,基本搜不到,不仅仅是相关问题搜不到,几乎就是没有结果. 再加上Js

  • JavaScript语法着色引擎(demo及打包文件下载)

    应 得意小蛇 的建议,我整理了一下去年写的JavaScript语法着色引擎,并提供下载,喜欢的尽管拿去,嘿嘿 总的来说是很简单的东西,只是提供了关键字的着色以及一些基本的语法(例如注释,字符串,正则等等),从demo中应该很容易看到其用法,这里简单介绍下: 类名:Lighter 通过new Lighter()可以得到一个着色引擎实例,假设为lighter,有以下属性和方法: 语言属性:lighter.language 这个属性的范围是可以根据语法文件的数量自己添加的,提供的demo中有'cpp'

  • javascript 多种搜索引擎集成的页面实现代码

    - 输入一个关键词,鼠标点击后面的搜索引擎链接,即可进入到该引擎的页面 - 如果输入关键词后敲回车,则使用默认搜索引擎,而每选择新的搜索引擎,默认引擎也会随之改变 - 自动记忆上次使用的搜索引擎,后面添加* 源代码如下,使用了多种IE/FF的适应办法: 复制代码 代码如下: <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=gb2312"&

  • 详解Javascript模板引擎mustache.js

    本文总结它的使用方法和一些使用心得,内容不算很高深,纯粹是入门内容,看看即可.不过要是你还没有用过此类的javascript引擎库,那么本文还是值得你一读的,相信在你了解完它强大的功能和简单用法之后,一定会迫不及待地将之用于你的工作当中. 1. 从一个简单真实的需求讲起 目前公司做了一个统一的开发平台,后台封装了MVC的接口和数据增删改查的接口,前端我自己用bootstrap+手写各类组件的方式弄了一套开发框架:集成了CAS,在CAS的基础上,首先做了一套统一权限管理系统,这个系统是我们开发平台

  • javascript轻量级模板引擎juicer使用指南

    使用方法 编译模板并根据数据立即渲染出结果 juicer(tpl, data); 仅编译模板暂不渲染,返回一个可重用的编译后的函数 var compiled_tpl = juicer(tpl); 根据给定的数据对之前编译好的模板进行渲染 var complied_tpl = juicer(tpl); var html = complied_tpl.render(data); 注册/注销自定义函数(对象) juicer.register('function_name', function); ju

  • 漫谈JS引擎的运行机制 你应该知道什么

    javascript从定义到执行,JS引擎在实现层做了很多初始化工作,因此在学习JS引擎工作机制之前,我们需要引入几个相关的概念:执行环境栈.全局对象.执行环境.变量对象.活动对象.作用域和作用域链等,这些概念正是JS引擎工作的核心组件.这篇文章的目的不是孤立的为你讲解每一个概念,而是通过一个简单的demo来展开分析,全局讲解JS引擎从定义到执行的每一个细节,以及这些概念在其中所扮演的角色. var x = 1; //定义一个全局变量 x function A(y){ var x = 2; //

  • node.js 使用ejs模板引擎时后缀换成.html

    这是一个小技巧,看着.ejs的后缀总觉得不爽,使用如下方法,可以将模板文件的后缀换成我们习惯的.html. 1.在app.js的头上定义ejs: 复制代码 代码如下: var ejs = require('ejs'); 2.注册html模板引擎: 复制代码 代码如下: app.engine('html',ejs.__express); 3.将模板引擎换成html: 复制代码 代码如下: app.set('view engine', 'html'); 4.修改模板文件的后缀为.html. 好了,任

  • Javascript 引擎工作机制详解

    Javascript 引擎工作机制 javascript从定义到执行,JS引擎在实现层做了很多初始化工作,因此在学习JS引擎工作机制之前,我们需要引入几个相关的概念:执行环境栈.全局对象.执行环境.变量对象.活动对象.作用域和作用域链等,这些概念正是JS引擎工作的核心组件.这篇文章的目的不是孤立的为你讲解每一个概念,而是通过一个简单的demo来展开分析,全局讲解JS引擎从定义到执行的每一个细节,以及这些概念在其中所扮演的角色. var x = 1; //定义一个全局变量 x function A

  • js动画(animate)简单引擎代码示例

    用惯了jquery的同学,相信都很欣赏其动画引擎.确实相对比较完善!如果,如果想像力足够丰富的话,相信可以做出超出想像的效果.当然,跟2d库比起来,还是相差相当一段距离.jquery压根也不是专门为动画而设计的.模拟真实世界方面,还是不足的.但在web世界里还是游刃有余的.动画其实一直是flash的专属领地(web区哉).只是它常常沦为黑客攻击的漏洞所在,而且要装插件,有时候文件实在太大,而且性耗实在是高啊.html5出现后,其实adobe自己都转移阵地到html5了.当然,我觉得很长一段时间内

  • 为JavaScript提供睡眠功能(sleep) 自编译JS引擎

    即然该功能如此需要,但为什么js中不提供这样的函数呢? 目前浏览器都是在UI线程解析js,以火狐浏览器为例,我重新编译了js引擎,并且在js引警中添加了sleep方法,该方法调用c语言的线程睡眠函数. 将方法附加到Object上,方法签名为sleep();无参数.默认休眠1秒钟,如果你在js中调用该函数,浏览器UI界面将被阻碍. 另外如果你的js函数存在死循环,浏览器的js解析会检测到js执行超时会提醒你是否终止执行本页面的js. 最后提供新编译的js引擎,将它们替换firefox下的js引擎即

  • jsp搜索引擎

    package coreservlets; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import java.net.*; public class SearchEngines extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws Servle

随机推荐