简单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


    本文转自魏琼东博客园博客,原文链接:http://www.cnblogs.com/dolphinX/p/3495256.html,如需转载请自行联系原作者

相关文章
|
15天前
|
JavaScript 前端开发 物联网
JavaScript:构建动态世界的引擎
JavaScript:构建动态世界的引擎
|
15天前
|
前端开发 JavaScript 开发者
JavaScript:构建动态网络的引擎
JavaScript:构建动态网络的引擎
|
5月前
|
监控 负载均衡 JavaScript
有哪些有效的方法可以优化Node.js应用的性能?
有哪些有效的方法可以优化Node.js应用的性能?
295 69
|
15天前
|
JavaScript 前端开发 开发者
JavaScript:驱动现代Web的核心引擎
JavaScript:驱动现代Web的核心引擎
|
15天前
|
JavaScript 前端开发 物联网
JavaScript:驱动现代Web的核心引擎
JavaScript:驱动现代Web的核心引擎
|
5月前
|
监控 算法 JavaScript
公司局域网管理视域下 Node.js 图算法的深度应用研究:拓扑结构建模与流量优化策略探析
本文探讨了图论算法在公司局域网管理中的应用,针对设备互联复杂、流量调度低效及安全监控困难等问题,提出基于图论的解决方案。通过节点与边建模局域网拓扑结构,利用DFS/BFS实现设备快速发现,Dijkstra算法优化流量路径,社区检测算法识别安全风险。结合WorkWin软件实例,展示了算法在设备管理、流量调度与安全监控中的价值,为智能化局域网管理提供了理论与实践指导。
129 3
|
11月前
|
Web App开发 JavaScript 前端开发
Node.js 是一种基于 Chrome V8 引擎的后端开发技术,以其高效、灵活著称。本文将介绍 Node.js 的基础概念
Node.js 是一种基于 Chrome V8 引擎的后端开发技术,以其高效、灵活著称。本文将介绍 Node.js 的基础概念,包括事件驱动、单线程模型和模块系统;探讨其安装配置、核心模块使用、实战应用如搭建 Web 服务器、文件操作及实时通信;分析项目结构与开发流程,讨论其优势与挑战,并通过案例展示 Node.js 在实际项目中的应用,旨在帮助开发者更好地掌握这一强大工具。
298 1
|
4月前
|
JavaScript
JS代码的一些常用优化写法
JS代码的一些常用优化写法
72 0
|
8月前
|
前端开发 JavaScript Java
JavaScript闭包深入剖析:性能剖析与优化技巧
JavaScript 闭包是强大而灵活的特性,广泛应用于数据封装、函数柯里化和事件处理等场景。闭包通过保存外部作用域的变量,实现了私有变量和方法的创建,提升了代码的安全性和可维护性。然而,闭包也可能带来性能问题,如内存泄漏和执行效率下降。为优化闭包性能,建议采取以下策略:及时解除对不再使用的闭包变量的引用,减少闭包的创建次数,使用 WeakMap 管理弱引用,以及优化闭包结构以减少作用域链查找的开销。在实际开发中,无论是 Web 前端还是 Node.js 后端,这些优化措施都能显著提升程序的性能和稳定性。
200 70
|
5月前
|
人工智能 监控 前端开发
基于 Next.js 的书法字体生成工具架构设计与 SSR 优化实践
本项目是一款书法字体生成工具,采用 Next.js 14(App Router)与 Tailwind CSS 构建前端,阿里云 Serverless 部署后端。通过混合渲染策略(SSG/SSR/CSR)、Web Worker 异步计算及 CDN 字体分片加载优化性能。服务端借助阿里云函数计算处理计算密集型任务,将平均耗时从 1200ms 降至 280ms,支持 1000+ QPS。动态路由与 ARMS 监控提升工程化水平,未来计划引入 WebGPU 和 AI 字体风格迁移技术,进一步优化用户体验。