簡單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模版引擎源碼能夠看看下面兩篇文章會有必定幫助

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

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

PS. 

  謝謝小灰狼的腦瓜幫忙找到原文連接

相關文章
相關標籤/搜索