在上篇博客最簡單的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 的寫法貼出來對比一下:
// Simple JavaScript Templating
// John Resig - http://ejohn.org/ - MIT Licensed
(function(){
var cache = {};
this.tmpl = function tmpl(str, data){
// Figure out if we're getting a template, or if we need to
// load the template - and be sure to cache the result.
var fn = !/\W/.test(str) ?
cache[str] = cache[str] ||
tmpl(document.getElementById(str).innerHTML) :
// Generate a reusable function that will serve as a template
// generator (and which will be cached).
new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments);};" +
// Introduce the data as local variables using with(){}
"with(obj){p.push('" +
// Convert the template into pure JavaScript
str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'")
+ "');}return p.join('');");
// Provide some basic currying to the user
return data ? fn( data ) : fn;
};
})();
.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);};
這句是幹什麼用的,看起來好像是測試的代碼,可以刪掉,有發現其它泳衣的同學告知一下啊。
優化後的版本
其實基本上也就是大神的原版上得一些改動
不是用with關鍵字處理作用域問題,使用call
添加處理轉義字符的置換語句
根據瀏覽器不同來決定使用+=還是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模版引擎源碼可以看看下面兩篇文章會有一定幫助。