简单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 的写法贴出来对比一下

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

 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);};

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

优化后的版本

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

  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模版引擎源码可以看看下面两篇文章会有一定帮助

http://news.cnblogs.com/n/139802/

http://cdc.tencent.com/?p=5723

PS. 

  谢谢小灰狼的脑瓜帮忙找到原文链接

posted @ 2013-12-31 09:32  谦行  阅读(3791)  评论(8编辑  收藏