简单JavaScript模版引擎优化

在上篇博客最简单的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 })();
复制代码

.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次,但这也算初步的一个优化工做了app

 push方法能够有多个参数

一直以来都在中规中矩的这样调用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还可以再优化

过于为何拼接字符串使用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);};

这句是干什么用的,看起来好像是测试的代码,能够删掉,有发现其它泳衣的同窗告知一下啊

优化后的版本

其实基本上也就是大神的原版上得一些改动

  1. 不是用with关键字处理做用域问题,使用call
  2. 添加处理转义字符的置换语句
  3. 根据浏览器不一样来决定使用+=仍是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模版引擎源码能够看看下面两篇文章会有必定帮助