在上篇博客最简单的JavaScript模板引擎 说了一下一个最简单的JavaScript模版引擎的原理与实现,做出了一个简陋的版本,今天优化一下,使之可以胜任平常拼接html工做,先把上次写的模版函数粘出来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 的写法贴出来对比一下正则表达式
1 // Simple JavaScript Templating 2 // John Resig - http://ejohn.org/ - MIT Licensed 3 (function(){ 4 var cache = {}; 5 6 this.tmpl = function tmpl(str, data){ 7 // Figure out if we're getting a template, or if we need to 8 // load the template - and be sure to cache the result. 9 var fn = !/\W/.test(str) ? 10 cache[str] = cache[str] || 11 tmpl(document.getElementById(str).innerHTML) : 12 13 // Generate a reusable function that will serve as a template 14 // generator (and which will be cached). 15 new Function("obj", 16 "var p=[],print=function(){p.push.apply(p,arguments);};" + 17 18 // Introduce the data as local variables using with(){} 19 "with(obj){p.push('" + 20 21 // Convert the template into pure JavaScript 22 str 23 .replace(/[\r\t\n]/g, " ") 24 .split("<%").join("\t") 25 .replace(/((^|%>)[^\t]*)'/g, "$1\r") 26 .replace(/\t=(.*?)%>/g, "',$1,'") 27 .split("\t").join("');") 28 .split("%>").join("p.push('") 29 .split("\r").join("\\'") 30 + "');}return p.join('');"); 31 32 // Provide some basic currying to the user 33 return data ? fn( data ) : fn; 34 }; 35 })();
咱们能够注意到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次,但这也算初步的一个优化工做了app
一直以来都在中规中矩的这样调用push方法ide
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而不是+=应该是由于在低版本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的本意是减小键盘输入。好比
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);};
这句是干什么用的,看起来好像是测试的代码,能够删掉,有发现其它泳衣的同窗告知一下啊
其实基本上也就是大神的原版上得一些改动
对应现代浏览器的版本大概是这样的
(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模版引擎源码能够看看下面两篇文章会有必定帮助