在上篇博客最簡單的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模版引擎源碼能夠看看下面兩篇文章會有必定幫助
http://news.cnblogs.com/n/139802/
http://cdc.tencent.com/?p=5723
PS.
謝謝小灰狼的腦瓜幫忙找到原文連接