精妙的 tmpl
前端模板類開源的不少,但最屬 jQuery 作者 John Resig 開發的 “javascript micro templating” 最為精妙,寥寥幾筆便實現了模板引擎核心功能。
它的介紹與使用方式請看作者部落格:http://ejohn.org/blog/javascript-micro-templating/
讓我們先看看他的源碼:
複製代碼 代碼如下:(function(){
var cache = {};
this.tmpl = function (str, data){
var fn = !/\W/.test(str) ?
cache[str] = cache[str] ||
tmpl(document.getElementById(str).innerHTML) :
new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments);};" +
"with(obj){p.push('" +
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('');");
return data ? fn( data ) : fn;
};
})();
麻雀雖小,五髒俱全,除了基本的資料附加外,還擁有緩衝機制、邏輯支援。現在,若要我評出一個javascript 最節能的自訂函數排名,第一名是 $ 函數(document.getElementById 簡版),而第二名就是 tmpl 了。
當然,它並非完美,我使用過程中發現了一些問題:
tmpl 美中不足
一、無法正確處理逸出字元,如: 複製代碼 代碼如下:tmpl('<%=name%>//<%=id%> ', {name:'糖餅', id: '1987'});
它就會報錯。若正常工作,它應該輸出:糖餅/1987
實際上解決起來很簡單,添加一行正則對轉義符進行轉義: 複製代碼 代碼如下:str.replace(/\\/g, "\\\\")
二、它有時候無法正確區分第一個參數是ID還是模板。
假若頁面模板ID帶有底線,如 tmpl-photo-thumb 它不會去尋找這個名稱的模板,會認為這傳入的是原始模板直接編譯輸出。
原始模板與元素id最直觀的區別就是是否含有空格,因此改動下Regex即可:
view sourceprint?1 !/\s/.test(str)
三、它內部還殘有一處測試用的代碼,可刪除。 複製代碼 代碼如下:print=function(){p.push.apply(p,arguments);}
tmpl 效率的疑惑
直到前段時間看了百度mux一篇介紹 YayaTemplate 的軟文,原文作者對各大流行的模板引擎進行了效率測試,最終得出 YayaTemplate 是最快的一個。 雖然測試結果 tmpl 不敵 YayaTemplate ,但也讓我打消了對效能的顧慮,實際應用中與傳統的字串拼接差不多。它們只有進行超大規模的解析才會有較大的效能差距。(超大規模?javascript本身就不適合幹這事。若哪天程式員一次性給瀏覽器插入上千條列表資料而其慢無比的時候,不用懷疑:問題出在了這個程式員身上,他不會愛惜使用者的瀏覽器。)
若說到引擎效率排名問題,我倒不覺得這是不能是衡量模板引擎的首要標準,模板文法也是重要的一環,這時候 YayaTemplate 的模板文法就顯得晦澀多了,它為了節省幾個Regex而在模板文法上耍了小聰明。
先展示 YayaTemplate 的源碼: 複製代碼 代碼如下://author:yaya,jihu
//uloveit.com.cn/template
//how to use? YayaTemplate("xxx").render({});
var YayaTemplate = YayaTemplate || function(str){
//核心分析方法
var _analyze=function(text){
return text.replace(/{\$(\s|\S)*?\$}/g,function(s){
return s.replace(/("|\\)/g,"\\$1")
.replace("{$",'_s.push("')
.replace("$}",'");')
.replace(/{\%([\s\S]*?)\%}/g, '",$1,"')
}).replace(/\r|\n/g,"");
};
//中間代碼
var _temp = _analyze(document.getElementById(str)?document.getElementById(str).innerHTML:str);
//返回產生器render方法
return {
render : function(mapping){
var _a = [],_v = [],i;
for (i in mapping){
_a.push(i);
_v.push(mapping[i]);
}
return (new Function(_a,"var _s=[];"+_temp+" return _s;")).apply(null,_v).join("");
}
}
};
若把效能問題上升到一個“學術問題”的高度嘗試去解決,為什麼 tmpl 會比 YayaTemplate 慢?
文法解析?雖然 YayaTemplate 使用了一個新穎的 javascript 包裹 html 的方式作為模板文法,但最終都需要用Regex解析成標準的 javascript 文法,這裡正則的效率不會有太大的差異,並且雙方都使用了緩衝機制確保只對原始模板僅進行一次解析。
資料轉換?模板引擎會把資料最終以變數的形式儲存在閉包中,以好讓模板擷取到。這裡先對比下一下雙方的變數聲明機制:
YayaTemplate 使用傳統傳遞參數的形式實現。它通過遍曆資料對象,把對象的名值分離,然後分別把對象成員名稱作為new Function的參數名(即變數名),然後使用函數的appley調用方式傳給那些參數。
tmpl 則使用了javascript不常用的 with 語句實現。 實現方式很簡潔,省去了var這個關鍵字。
tmpl 效能問題就出在 with 上面。javascript 提供的 with 語句,本意是想用來更快捷的訪問對象的屬性。不幸的是,with語句在語言中的存在,就嚴重影響了 javascript 引擎的速度,因為它阻止了變數名的詞法範圍綁定。
最佳化 tmpl
tmpl 若去掉 with 語句,而改用傳統的傳參效能立即大提升,經過實測在24萬條資料下 firefox 能提高 5 倍,chrome 2.4 倍,opera 1.84倍,safari 2.1倍,IE6 1.1倍,IE9 1.35倍,最終與 YayaTemplate 不分上下。
測試地址:http://www.planeart.cn/demo/tmpl/tmpl.html
tmpl 最佳化版最終代碼: 複製代碼 代碼如下:/**
* 微型模板引擎 tmpl 0.2
*
* 0.2 更新:
* 1. 修複逸出字元與id判斷的BUG
* 2. 放棄低效的 with 語句從而最高提升3.5倍的執行效率
* 3. 使用隨機內部變數防止與模板變數產生衝突
*
* @author John Resig, Tang Bin
* @see http://ejohn.org/blog/javascript-micro-templating/
* @name tmpl
* @param {String} 模板內容或者裝有模板內容的元素ID
* @param {Object} 附加的資料
* @return {String} 解析好的模板
*
* @example
* 方式一:在頁面嵌入模板
* <script type="text/tmpl" id="tmpl-demo">
* <ol title="<%=name%>">
* <% for (var i = 0, l = list.length; i < length; i ++) { %>
* <li><%=list[i]%></li>
* <% } %>
* </ol>
* </script>
* tmpl('tmpl-demo', {name: 'demo data', list: [202, 96, 133, 134]})
*
* 方式二:直接傳入模板:
* var demoTmpl =
* '<ol title="<%=name%>">'
* + '<% for (var i = 0, l = list.length; i < length; i ++) { %>'
* + '<li><%=list[i]%></li>'
* + '<% } %>'
* +'</ol>';
* var render = tmpl(demoTmpl);
* render({name: 'demo data', list: [202, 96, 133, 134]});
*
* 這兩種方式區別在於第一個會自動緩衝編譯好的模板,
* 而第二種緩衝交給外部對象控制,如例二中的 render 變數。
*/
var tmpl = (function (cache, $) {
return function (str, data) {
var fn = !/\s/.test(str)
? cache[str] = cache[str]
|| tmpl(document.getElementById(str).innerHTML)
: function (data) {
var i, variable = [$], value = [[]];
for (i in data) {
variable.push(i);
value.push(data[i]);
};
return (new Function(variable, fn.$))
.apply(data, value).join("");
};
fn.$ = fn.$ || $ + ".push('"
+ str.replace(/\\/g, "\\\\")
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join($ + ".push('")
.split("\r").join("\\'")
+ "');return " + $;
return data ? fn(data) : fn;
}})({}, '$' + (+ new Date));
模板引擎依賴 Function 構造器實現,它與 eval 一樣提供了使用文本訪問 javascript 解析引擎的方法,這也會讓效能顯著的降低,但此時 javascript 中已別無他法。
使用 Function 構造器還會對參數名稱有所限制,所以導致資料成員命名必須與 javascript 變數名規範保持一致,否則會報錯。好在這個錯誤可以在啟動並執行時候立馬被發現,而不會成為一顆地雷。
tmpl 使用小竅門
一、緩衝最佳化。
tmpl 預設對嵌入到頁面中的模板進行了緩衝最佳化(即第一個參數為ID的時候),它只會對模板進行一次分析。若原始模板是直接傳入到 tmpl 第一個參數中,且需要多次使用的話,建議用公用變數緩衝起來,需要解析資料的時候再使用,以獲得相同的最佳化效果。如: 複製代碼 代碼如下:// 產生模板緩衝
var render = tmpl(listTmpl);
// 可多次調用模板
elem.innerHTML = render(data1);
elem.innerHTML = render(data2);
...
二、避免未定義的變數引起系統崩潰。
若模板中定義了一個變數輸出,而且傳入資料卻少了這個項目就會出現變數未定義的錯誤,從而引起整個程式的崩潰。如果無法確保資料完整性,仍然有方法可以對對其成員進行探測。原版中暗含變數儲存了原始傳入的資料,即 obj ;而在我的升級版本中則是關鍵字 this,如: 複製代碼 代碼如下:<% if (this.dataName !== undefined) { %>
<%=dataName %>
<% } %>
三、調試模板。
由於模板引擎是用文本的調用的 javascript 引擎,調試工具無法定位到出錯的行。在 升級版本 中你可以用調試工具輸出編譯好的模板緩衝。例如調試這個模板: 複製代碼 代碼如下:<script id="tmpl" type="text/tmpl">
<ul>
<% for (var i = 0, l = list.length; i < l; i ++) { %>
<li><%=list[i].index%>. 使用者: <%=list[i].user%>; 網站:<%=list[i].site%></li>
<% } %>
</ul>
輸出緩衝: 複製代碼 代碼如下:window.console(tmpl('tmpl').$);
日誌結果: 複製代碼 代碼如下:"$1318348744541.push('
<ul> '); for (var i = 0, l = list.length; i < l; i ++) { $1318348744541.push('
<li>',list[i].index,'. 使用者: ',list[i].user,'; 網站:',list[i].site,'</li>
'); } $1318348744541.push(' </ul>
');return $1318348744541"
現在你可以看到模板引擎編譯好的javascript語句,可以對照這檢查模板是否存在錯誤。($1318348744541是一個隨機名稱的臨時數組,可忽略)
最後非常感謝 tmpl 原作者 與 YayaTemplate 作者的付出,正因為此我才有機會深入分析實現機制,解決問題並從中受益。獨樂不如眾樂,分享之。
唐斌 – 2011.10.09 – 湖南-長沙