javascript模版引擎-tmpl的bug修復與性能優化


在平時編碼中,經常要做拼接字符串的工作,如把json數據用HTML展示出來,以往字符串拼接與邏輯混在在一起會讓代碼晦澀不堪,加大了多人協作與維護的成本。而采用前端模板機制就能很好的解決這個問題。

精妙的 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最直觀的區別就是是否含有空格,因此改動下正則表達式即可:

!/\s/.test(str)

三、它內部還殘有一處測試用的代碼,可刪除。

print=function(){p.push.apply(p,arguments);}

tmpl 效率的疑惑

直到前段時間看了百度mux一篇介紹 YayaTemplate 的軟文,原文作者對各大流行的模板引擎進行了效率測試,最終得出 YayaTemplate 是最快的一個。 雖然測試結果 tmpl 不敵 YayaTemplate ,但也讓我打消了對性能的顧慮,實際應用中與傳統的字符串拼接差不多。它們只有進行超大規模的解析才會有較大的性能差距。(超大規 模?javascript本身就不適合干這事。若哪天程序員一次性給瀏覽器插入上千條列表數據而其慢無比的時候,不用懷疑:問題出在了這個程序員身上,他 不會愛惜用戶的瀏覽器。)

若說到引擎效率排名問題,我倒不覺得這是不能是衡量模板引擎的首要標准,模板語法也是重要的一環,這時候 YayaTemplate 的模板語法就顯得晦澀多了,它為了節省幾個正則表達式而在模板語法上耍了小聰明。

先展示 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 的方式作為模板語法,但最終都需要用正則表達式解析成標准的 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 作者的付出,正因為此我才有機會深入分析實現機制,解決問題並從中受益。獨樂不如眾樂,分享之。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM