简单JavaScript模版引擎优化
在上篇博客最简单的JavaScript模板引擎 说了一下一个最简单的JavaScript模版引擎的原理与实现,作出了一个简陋的版本,今天优化一下,使之能够胜任日常拼接html工作,先把上次写的模版函数粘出来
function tmpl(id,data){
var html=document.getElementById(id).innerHTML;
var result="var p=[];with(obj){p.push('"
+html.replace(/[\r\n\t]/g," ")
.replace(/<%=(.*?)%>/g,"');p.push($1);p.push('")
.replace(/<%/g,"');")
.replace(/%>/g,"p.push('")
+"');}return p.join('');";
var fn=new Function("obj",result);
return fn(data);
}
顺便也把John Resing 的写法贴出来对比一下
// Simple JavaScript Templating
// John Resig - http://ejohn.org/ - MIT Licensed
(function(){
var cache = {}; this.tmpl = function tmpl(str, data){
// Figure out if we're getting a template, or if we need to
// load the template - and be sure to cache the result.
var fn = !/\W/.test(str) ?
cache[str] = cache[str] ||
tmpl(document.getElementById(str).innerHTML) : // Generate a reusable function that will serve as a template
// generator (and which will be cached).
new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments);};" + // Introduce the data as local variables using with(){}
"with(obj){p.push('" + // Convert the template into pure JavaScript
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('');"); // Provide some basic currying to the user
return data ? fn( data ) : fn;
};
})();
.split("xxx").join("")是不是比replace效率高
我们可以注意到John Resig在替换简单字符串的时候并不是利用的replace函数,而是使用的.split('xxx').join('')这样的形式,乍一看我没明白是什么意思,类似这样
.split("\t").join("');")
仔细看了两眼,达到的效果就是字符串替换,但是不明白为什么复杂的(需要使用正则表达式的)使用replace,简单的却使用.split('XXX').join('')这样的方式,莫非是执行效率问题?自己动手做了个例子验证一下
for(var n=0;n<10;n++){
var a="<%=123><%gdfgsfdbgsfdb><%%>", i=0, t1=null, t2=null, span1=0, span2=0;
t1=new Date();
while(i<9000000){
a.replace(/<%/g,"asdas");
i++;
}
t2=new Date(); span1=t2.getTime()-t1.getTime(); i=0;
t1=new Date();
while(i<9000000){
a.split("<%").join("asdas");
i++;
}
t2=new Date(); span2=t2.getTime()-t1.getTime(); console.log(span1+"\t"+span2);
}
不看不知道,一看吓一跳,如果我们希望replace方法替换字符串中所有指定字符串而不是只替换一次,那么就得往replace里传入正则表达式参数,并声明全局属性替换,这样的话和.split('XXX').join('')效率上得差距还是有一些的,看看测试结果
图中可以看出来,在一个并不是很复杂的字符串中替换三次,使用replace就有一定的劣势了,当然我们实际用的时候不会像替换测试中使用9000000次,但这也算初步的一个优化工作了
push方法可以有多个参数
一直以来都在中规中矩的这样调用push方法
a.push('xxx');
殊不知push方法可以传入多个参数,按顺序把参数放入数组,类似这样
p.push('xxx','ooo');
我们可以看到John Resig并不是简单的把 <%=xxx%> 替换为 ');p.push(xxx);p.push(',而是通过
<% => \t
\t=xxx%> => ',$1,'
\t => ');
这样达到了一次push函数放入多个参数,减少了push函数的调用次数,这样原来拼接为
p.push('<ul>');
for(var i=0;i<users.length;i++){
p.push('<li><a href="');
p.push(users[i].url);
p.push('">');
p.push(users[i].name);
p.push('</a></li>');
}
p.push('</ul>');
现在变成了下面内容,调用方法次数减少了,理论上也是可以在效率上有一定优化效果的(未测试)
p.push('<ul>');
for(var i=0;i<users.length;i++){
p.push('<li><a href="', users[i].url, '">', users[i].name, '</a></li>');
}
p.push('</ul>');
其实push还能够再优化
过于为什么拼接字符串使用push而不是+=应该是因为在低版本IE(IE 6-8)下频繁调用字符串+=效率比较低,据可靠消息透露,其实在现代浏览器中使用+=拼接字符串的效率是要比使用push高出不少的,所以这里我们可以根据浏览器不同使用不同的方式拼接字符串,在一定程度上优化模版引擎效率
在高版本(IE9+)和现代浏览器上我们可以使用一套新的替换法则,使用+=拼接字符串而不是push方法,法则很简单
<%=xxx%> => ';+xxx+' <% => '; %> => p+='
方法写出来后类似于这样
function tmpl(id,data){
var html=document.getElementById(id).innerHTML;
var result="var p='';with(obj){p+='"
+html.replace(/[\r\n\t]/g," ")
.replace(/<%=(.*?)%>/g,"'+$1+'")
.replace(/<%/g,"';")
.replace(/%>/g,"p+='")
+"';}return p;";
var fn=new Function("obj",result);
return fn(data);
}
with产生的效率问题
我们当时为了解决作用域问题使用了with关键字,但是这个模版引擎的很大一部分效率问题正是犹豫with产生的,with的本意是减少键盘输入。比如
obj.a = obj.b; obj.c = obj.d;
可以简写成
with(obj) {
a = b;
c = d;
}
但是,在实际运行时,解释器会首先判断obj.b和obj.d是否存在,如果不存在的话,再判断全局变量b和d是否存在。这样就导致了低效率,而且可能会导致意外,因此最好不要使用with语句。
在JavaScript中除了with,apply和call函数也可以改变JavaScript代码执行环境,因此我们可以使用call函数,这样因为使用with而导致的性能问题就可以得到优化
function tmpl(id,data){
var html=document.getElementById(id).innerHTML;
var result="var p='';p+='"
+html.replace(/[\r\n\t]/g," ")
.replace(/<%=(.*?)%>/g,"'+$1+'")
.replace(/<%/g,"';")
.replace(/%>/g,"p+='")
+"';return p;";
var fn=new Function("obj",result);
return fn.call(data);
}
缓存模版
我们可以看到John Resig在处理的时候加入了一个cache对象,并不是每次调用模版引擎的时候都会替换字符串,他会把每次解析的模版保存下来,以备下次使用,我们之前让模版引擎方法接受两个参数分别是模版的id和数据源,John Resig使用的方法,第一个参数可以是id或者是模版内容,为了看清楚其作用,我们简写一下他的方法,去掉外层立即执行函数的部分
this.tmpl = function tmpl(str, data){
var fn = !/\W/.test(str) ?
cache[str] = cache[str] || tmpl(document.getElementById(str).innerHTML) :
new Function("obj",bodyStr); return data ? fn( data ) : fn;
};
在调用tmpl方法的时候他会检查第一个参数,如果参数中包含非单词部分(空格回车神马的),就认为其传入的是模版内容,否则认为其传入的是模版id(按照这个正则表达式,如果模版id中用 - 那么也会被认为是模版内容,但是id中带有-本身就很奇怪,如果有这种可能,可以改为 /[\W|-]/)。当传入的是模版内容的时候执行刚才我们写的new Function("obj",body)部分构造一个新函数;当传入的是模版id的时候会判断cache是否有缓存,如果没有把根据id获取的模版内容作为第一个参数传入自身,再调用一次,把结果放入缓存。
这样处理的效果就是每次我们调用模版的时候,如果传入的是模版内容,那么它会构造一个新的函数,如果使用的是模版id的话,第一次使用后会把构造好的方法放入缓存,这样再次调用的时候就不用解析模版内容,生成新函数了。有同学可能会问,我们会重复调用模版方法吗,很可能会,比如我写了个模版是输出一个学生信息的模版,我想再页面render一个班的学生信息,可能就会使用模版数十次,只是每次传入的数据不同而已,所以这个优化还是很有必要的。简单修改一下方法加上缓存功能
(function(){
var cache={};
this.tmpl=function(str,data){
var fn= !/\s/.test(str) ?
cache[str]=cache[str] || tmpl(document.getElementById(str).innerHTML) :
new Function("obj","var p='';p+='"
+str.replace(/[\r\n\t]/g," ")
.replace(/<%=(.*?)%>/g,"'+$1+'")
.replace(/<%/g,"';")
.replace(/%>/g,"p+='")
+"';return p;"); return data? fn.call(data):fn;
}
})();
特殊字符处理的优化
对比一下我们发现John Resig再构造新方法的时候多处理了几个replace,主要是防止模版内容出现 ' ,这个东西会影响我们拼接字符串,所以先把它替换为换行符,处理完其它的后再把换行符转换为转义的' 即\\',说到这里我们发现其实大神也难免有疏忽的时候,要是模版中有转义字符\,也会对字符串拼接产生影响,所以我们需要多加一个置换 .split("\\").join("\\\\") 来消除转义字符的影响。
当然不太明白大神代码中的
print=function(){p.push.apply(p,arguments);};
这句是干什么用的,看起来好像是测试的代码,可以删掉,有发现其它泳衣的同学告知一下啊
优化后的版本
其实基本上也就是大神的原版上得一些改动
- 不是用with关键字处理作用域问题,使用call
- 添加处理转义字符的置换语句
- 根据浏览器不同来决定使用+=还是push方法拼接字符串(这个因为没有想清楚是使用惰性载入函数还是针对浏览器写两个函数开发者自己选择调用,所以就不在代码中体现了,有兴趣同学可以使用自己觉得合适的方式实现)
对应现代浏览器的版本大概是这样的
(function(){
var cache={};
this.tmpl=function(str,data){
var fn= !/\s/.test(str) ?
cache[str]=cache[str] || tmpl(document.getElementById(str).innerHTML) :
new Function("obj","var p='';p+='"
+str.replace(/[\r\n\t]/g," ")
.split('\\').join("\\\\")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "'+$1+'")
.split("\t").join("';")
.split("%>").join("p+='")
.split("\r").join("\\'")
+"';return p;"); return data? fn.call(data):fn;
}
})();
最后
虽然优化工作做完了,但这只是最简单的一个模版引擎,其它的一些强大的模版引擎不但在语法上支持注释语句,甚至添加调试和报错行数支持,这个并没有处理这些内容,但我觉得在日常开发中已经够用了。对于调试、报错等方面有兴趣的同学除了一些成熟的JavaScript模版引擎源码可以看看下面两篇文章会有一定帮助
http://news.cnblogs.com/n/139802/
http://cdc.tencent.com/?p=5723
PS.
谢谢小灰狼的脑瓜帮忙找到原文链接
简单JavaScript模版引擎优化的更多相关文章
- Javascript模版引擎简介
回顾 Micro-Templating 出自John Resig 2008年的一片文章,以及其经典实现: // Simple JavaScript Templating // John Resig - ...
- Javascript模版引擎mustache.js简介
背景 最近使用ELK的sentinl进行告警配置,sentinl的邮件通知支持mustache,借此机会学习了mustache相关知识,记录在此. mustache的思想 mustache的核心是标签 ...
- 最简单的JavaScript模板引擎
在小公司待久了感觉自己的知识面很小,最近逛博客园和一些技术网站看大家在说JavaScript模版引擎的事儿,完全没有概念,网上一搜这是08年开始流行起来的...本来以为这是很高深的知识,后来在网上看到 ...
- 如何在前端模版引擎开发中避免使用eval函数
前段时间,想着自己写一个简单的模版引擎,便于自己平时开发demo时使用.于是根据自己对模版引擎的理解,定义自己的模版格式,然后,根据自己定义的格式,编写处理函数,将模版标签中的字符串,解析成可执行的字 ...
- js模版引擎开发实战以及对eval函数的改进
简介 前段时间,想着自己写一个简单的模版引擎,便于自己平时开发demo时使用,同时也算是之前学习的知识的一种总结吧! 首先我们先了解一下模版引擎的工作原理吧! 1. 模版引擎其实就是将指定标签的内容根 ...
- 探究Javascript模板引擎mustache.js使用方法
这篇文章主要为大家介绍了Javascript模板引擎mustache.js使用方法,mustache.js是一个简单强大的Javascript模板引擎,使用它可以简化在js代码中的html编写,压缩后 ...
- MGTemplateEngine 模版引擎简单使用(转)
原文:http://blog.csdn.net/crazy_srufboy/article/details/21748995 要实现的效果 首先上图中间的 标题至内容 都是使用UIWebView显示, ...
- 【转载】Asp.Net中使用基于jQuery的javascript前台模版引擎JTemplate
JTemplate是基于jQuery的开源的前端模版引擎,在Jtemplate模板中可以使用if判断.foreach循环.for循环等操作,使用Jtemplate模板优点在于ajax局部刷新界面时候不 ...
- [转载]Juicer – 一个Javascript模板引擎的实现和优化
http://ued.taobao.org/blog/2012/04/juicer-%E4%B8%80%E4%B8%AAjavascript%E6%A8%A1%E6%9D%BF%E5%BC%95%E6 ...
随机推荐
- SpringMVC 结合HttpClient调用第三方接口实现
使用HttpClient 依赖jar包 1:commons-httpclient-3.0.jar 2:commons-logging-1.1.1.jar 3:commons-codec-1.6.jar ...
- UVa 11292 Dragon of Loowater
简单贪心 龙头的直径和人的佣金排序,价值小的人和直径小的配 #include<iostream> #include<cstdio> #include<cmath> ...
- ulipad源码包配置环境及安装
一.准备下载的安装包: 1.python(我电脑配置的是2.7)下载地址http://pan.baidu.com/s/1qWrGZk4 2.wxpython(我这里是wxpy3.0,配套python2 ...
- ELK 5.0 组件后台启动
elasticsearch 后台启动,只需要 在bin目录下执行: ./elasticsearch -d 查看是否启动成功使用: ps aux|grep elasticsearch kibana 后台 ...
- IOCP入门
完成端口(Completion Port)详解 此文讲解最好,也很全面一下其他文章看看就行,也可不看. 单句柄数据,单IO数据 此文讲述比较清晰,可以辅助理解上文. IOCP编程之基本原理:http: ...
- 最喜欢的算法(们) - Levenshtein distance
String Matching: Levenshtein distance Purpose: to use as little effort to convert one string into th ...
- [UCSD白板题] Changing Money
Problem Introduction In this problem,you will design an algorithm for changing money optimally. Prob ...
- CAP定理
from wikipedia CAP定理 CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer's theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点: 一致 ...
- H5项目常见问题
转自 https://github.com/FrontEndZQ/HTML5-FAQH5项目常见问题及注意事项 Meta基础知识: H5页面窗口自动调整到设备宽度,并禁止用户缩放页面//一.HTML页 ...
- Select loop
The Bash Shell also offer select Loop, the syntax is: select varName in list do command1 command2 .. ...