JavaScript template language shootoff (v741)

Revision 741 of this benchmark created on


Description

A limited comparison of some popular JavaScript templating engines on a short template: 6 header tags, and 10 list items. Compared templating engines:

Preparation HTML

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>

<script src="http://documentcloud.github.com/underscore/underscore.js"></script>

<!--NOTE: Need to load from CDNJS since GitHub uses text/plain MIME type, which is blocked in IE9-->
<!--<script src="http://github.com/janl/mustache.js/raw/master/mustache.js"></script>-->
<script src="http://ajax.cdnjs.com/ajax/libs/mustache.js/0.3.0/mustache.min.js"></script>

<script src="https://github.com/downloads/wycats/handlebars.js/handlebars.1.0.0.beta.3.js"></script>

<script src="http://cdn.kendostatic.com/2011.2.804/js/kendo.all.min.js"></script>

<script src="http://jashkenas.github.com/coffee-script/extras/coffee-script.js"></script>

<!--NOTE: Need to load from MSFT CDN since GitHub uses text/plain MIME type, which is blocked in IE9-->
<!--<script src="http://github.com/jquery/jquery-tmpl/raw/master/jquery.tmpl.min.js"></script>-->
<script src="http://ajax.microsoft.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js"></script>

<script src="http://qatrix.com/js/qatrix-1.1.min.js"></script>

<script>
var $ = jQuery.noConflict();
  window.sharedVariables = {
   header: "Header",
   header2: "Header2",
   header3: "Header3",
   header4: "Header4",
   header5: "Header5",
   header6: "Header6",
   list: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
  };
 

var templateA = function (id, content) {
    return template[
        typeof content === 'object' ? 'render' : 'compile'
    ].apply(template, arguments);
};




(function (exports, global) {


//"use strict";
exports.openTag = '<@';
exports.closeTag = '@>';
exports.parser = null;



/**
 * 渲染模板
 * @name    template.render
 * @param   {String}    模板ID
 * @param   {Object}    数据
 * @return  {String}    渲染好的HTML字符串
 */
exports.render = function (id, data) {

    var cache = _getCache(id);
    
    if (cache === undefined) {

        return _debug({
            id: id,
            name: 'Render Error',
            message: 'Not Cache'
        });
        
    }
    
    return cache(data); 
};



/**
 * 编译模板
 * 2012-6-6:
 * define 方法名改为 compile,
 * 与 Node Express 保持一致,
 * 感谢 TooBug 提供帮助!
 * @name    template.compile
 * @param   {String}    模板ID (可选)
 * @param   {String}    模板字符串
 * @return  {Function}  渲染方法
 */
exports.compile = function (id, source) {
    
    var debug = arguments[2];
    
    
    if (typeof source !== 'string') {
        debug = source;
        source = id;
        id = null;
    }

    
    try {
        
        var Render = _compile(source, debug);
        
    } catch (e) {
    
        e.id = id || source;
        e.name = 'Syntax Error';
        return _debug(e);
        
    }
    
    
    function render (data) {
        
        try {
            
            return new Render(data).template;
            
        } catch (e) {
            
            if (!debug) {
                return exports.compile(id, source, true)(data);
            }
                        
            e.id = id || source;
            e.name = 'Render Error';
            e.source = source;
            
            return _debug(e);
            
        };
        
    };
    
    render.prototype = Render.prototype;
    render.toString = function () {
        return Render.toString();
    };
    
    
    if (id) {
        _cache[id] = render;
    }

    
    return render;

};




/**
 * 扩展模板公用辅助方法
 * @name    template.helper
 * @param   {String}    名称
 * @param   {Function}  方法
 */
exports.helper = function (name, helper) {
    _helpers[name] = helper;
};




var _cache = {};
var _isNewEngine = ''.trim;
var _isServer = _isNewEngine && !global.document;
var _keyWordsMap = {};
var forEach =  Array.prototype.forEach;



var _forEach = function (array, callback) {
        forEach.call(array, callback);
};

var _create = Object.create || function (object) {
    function Fn () {};
    Fn.prototype = object;
    return new Fn;
};



var _helpers = exports.prototype = {
    $forEach: _forEach,
    $render: exports.render,
    $getValue: function (value) {
        return value === undefined ? '' : value;
    }
};



// javascript 关键字表
_forEach((

    // 关键字
    'break,case,catch,continue,debugger,default,delete,do,else,false,finally,for,function,if'
    + ',in,instanceof,new,null,return,switch,this,throw,true,try,typeof,var,void,while,with'
    
    // 保留字
    + ',abstract,boolean,byte,char,class,const,double,enum,export,extends,final,float,goto'
    + ',implements,import,int,interface,long,native,package,private,protected,public,short'
    + ',static,super,synchronized,throws,transient,volatile'
    
    // ECMA 5 - use strict
    + ',arguments,let,yield'
    
).split(','), function (key) {
    _keyWordsMap[key] = true;
});




// 模板编译器
var _compile = function (source, debug) {
    

    var openTag = exports.openTag;
    var closeTag = exports.closeTag;
    var parser = exports.parser;

    
    var code = source;
    var tempCode = '';
    var line = 1;
    var uniq = {$out:true,$line:true};
    
    var variables = "var $helpers=this,"
    + (debug ? "$line=0," : "");


    var replaces = _isNewEngine
    ? ["$out='';", "$out+=", ";", "$out"]
    : ["$out=[];", "$out.push(", ");", "$out.join('')"];

    var concat = _isNewEngine
        ? "if(content!==undefined){$out+=content;return content}"
        : "$out.push(content);";
          
    var print = "function(content){" + concat + "}";

    var include = "function(id,data){"
    +     "if(data===undefined){data=$data}"
    +     "var content=$helpers.$render(id,data);"
    +     concat
    + "}";
    
    
    // html与逻辑语法分离
    _forEach(code.split(openTag), function (code, i) {
        code = code.split(closeTag);
        
        var $0 = code[0];
        var $1 = code[1];
        
        // code: [html]
        if (code.length === 1) {
            
            tempCode += html($0);
         
        // code: [logic, html]
        } else {
            
            tempCode += logic($0);
            
            if ($1) {
                tempCode += html($1);
            }
        }
        

    });
    
    
    
    code = tempCode;
    
    
    // 调试语句
    if (debug) {
        code = 'try{' + code + '}catch(e){'
        +       'e.line=$line;'
        +       'throw e'
        + '}';
    }
    
    
    code = variables + replaces[0] + code + 'this.template=' + replaces[3];
    
    
    try {
        
        var render = new Function('$data', code);
        var proto = render.prototype = _create(_helpers);
        proto.toString = function () {
            return this.template;
        };

        return render;
        
    } catch (e) {
        e.temp = 'function anonymous($data) {' + code + '}';
        throw e;
    };
    
    
    
    // 处理 HTML 语句
    function html (code) {
        
        // 记录行号
        line += code.split(/\n/).length - 1;
        
        code = code
        // 单双引号与反斜杠转义
        .replace(/('|"|\\)/g, '\\$1')
        // 换行符转义(windows + linux)
        .replace(/\r/g, '\\r')
        .replace(/\n/g, '\\n');
        
        code = replaces[1] + "'" + code + "'" + replaces[2];
        
        return code + '\n';
    };
    
    
    // 处理逻辑语句
    function logic (code) {

        var thisLine = line;
       
        if (parser) {
        
             // 语法转换器
            code = parser(code);
            
        } else if (debug) {
        
            // 记录行号
            code = code.replace(/\n/g, function () {
                line ++;
                return '$line=' + line +  ';';
            });
            
        }
        
        
        // 输出语句
        if (code.indexOf('-') === 0) {

            code = code.substring(1).replace(/[\s;]*$/, '');
            
            if (_isNewEngine) {
                // $getValue: undefined 转化为空字符串
                code = '$getValue(' + code + ')';
            }

            code = replaces[1] + code + replaces[2];

        }
        
        if (debug) {
            code = '$line=' + thisLine + ';' + code;
        }
                
        getKey(code);
        
        return code + '\n';
    };
    
    
    // 提取模板中的变量名
    function getKey (code) {
        
        // 过滤注释、字符串、方法名
        code = code.replace(/\/\*.*?\*\/|'[^']*'|"[^"]*"|\.[\$\w]+/g, '');
                
        // 分词
        _forEach(code.split(/[^\$\w\d]+/), function (name) {
         
            // 沙箱强制语法规范:禁止通过套嵌函数的 this 关键字获取全局权限
            if (/^this$/.test(name)) {
                throw {
                    message: 'Prohibit the use of the "' + name + '"'
                };
            }
                        
            // 过滤关键字与数字
            if (!name || _keyWordsMap.hasOwnProperty(name) || /^\d/.test(name)) {
                return;
            }
            
            // 除重
            if (!uniq.hasOwnProperty(name)) {
                setValue(name);
                uniq[name] = true;
            }
            
        });
        
    };
    
    
    // 声明模板变量
    // 赋值优先级: 内置特权方法(include, print) > 公用模板辅助方法 > 数据
    function setValue (name) {  
        var value;
        var origins = ",Math,encodeURI,encodeURIComponent,decodeURI,decodeURIComponent,escape,unescape,eval\
        ,isNaN,isFinite,parseInt,parseFloat\
        ,undefined,NaN,Infinity,Object,Array,Function,Boolean,String,Number,Date,RegExp,Error,";

        if(~origins.indexOf(","+name+",")){
            return;
        }

        if (name === 'print') {

            value = print;

        } else if (name === 'include') {
        
            value = include;
            
        } else if (_helpers.hasOwnProperty(name)) {
            
            value = '$helpers.' + name;
            
        } else {
        
            value = '$data.' + name;
        
        }
        
        variables += name + '=' + value + ',';
    };
    
        
};



// 获取模板缓存
var _getCache = function (id) {

    var cache = _cache[id];
    
    if (cache === undefined && !_isServer) {
        var elem = document.getElementById(id);
        
        if (elem) {
            exports.compile(id, elem.value || elem.innerHTML);
        }
        
        return _cache[id];
        
    } else if (_cache.hasOwnProperty(id)) {
    
        return cache;
    }
};



// 模板调试器
var _debug = function (e) {

    var content = '[template]:\n'
        //+ e.id
        + '\n[name]:\n'
        + e.name;
    
    if (e.message) {
        content += '\n[message]:\n'
        + e.message;
    }
    
    if (e.line) {
        content += '\n[line]:\n'
        + e.line;
        content += '\n[source]:\n'
        + e.source.split(/\n/)[e.line - 1].replace(/^[\s\t]+/, '');
    }
    
    if (e.temp) {
        content += '\n[temp]:\n'
        + e.temp;
    }
    
    function error () {
        return error + '';
    };
    
    error.toString = function () {
        return '{Template Error}'+"\n"+content;
    };
    
    return error;
};



})(templateA, this);



  window.jQueryTemplate = $.template(null, "<div><h1 class='header'>{{html header}}</h1><h2 class='header2'>{{html header2}}</h2><h3 class='header3'>{{html header3}}</h3><h4 class='header4'>{{html header4}}</h4><h5 class='header5'>{{html header5}}</h5><h6 class='header6'>{{html header6}}</h6><ul class='list'>{{each list}}<li class='item'>{{html $value}}</li>{{/each}}</ul></div>");
  
  window.mustacheTemplate = "<div><h1 class='header'>{{{header}}}</h1><h2 class='header2'>{{{header2}}}</h2><h3 class='header3'>{{{header3}}}</h3><h4 class='header4'>{{{header4}}}</h4><h5 class='header5'>{{{header5}}}</h5><h6 class='header6'>{{{header6}}}</h6><ul class='list'>{{#list}}<li class='item'>{{{.}}}</li>{{/list}}</ul></div>";
  
  window.handlebarsTemplate = Handlebars.compile("<div><h1 class='header'>{{header}}</h1><h2 class='header2'>{{header2}}</h2><h3 class='header3'>{{header3}}</h3><h4 class='header4'>{{header4}}</h4><h5 class='header5'>{{header5}}</h5><h6 class='header6'>{{header6}}</h6><ul class='list'>{{#each list}}<li class='item'>{{this}}</li>{{/each}}</ul></div>");
  
  window.kendouiTemplate = kendo.template("<div><h1 class='header'><#= data.header #></h1><h2 class='header2'><#= data.header2 #></h2><h3 class='header3'><#= data.header3 #></h3><h4 class='header4'><#= data.header4 #></h4><h5 class='header5'><#= data.header5 #></h5><h6 class='header6'><#= data.header6 #></h6><ul class='list'><# for (var i = 0, l = data.list.length; i < l; i++) { #><li class='item'><#= data.list[i] #></li><# } #></ul></div>", {useWithBlock:true});
  
  window.underscoreTemplate = _.template("<div><h1 class='header'><%= header %></h1><h2 class='header2'><%= header2 %></h2><h3 class='header3'><%= header3 %></h3><h4 class='header4'><%= header4 %></h4><h5 class='header5'><%= header5 %></h5><h6 class='header6'><%= header6 %></h6><ul class='list'><% for (var i = 0, l = list.length; i < l; i++) { %><li class='item'><%= list[i] %></li><% } %></ul></div>");
  
  window.baseHtml = "<div><h1 class='header'></h1><h2 class='header2'></h2><h3 class='header3'></h3><h4 class='header4'></h4><h5 class='header5'></h5><h6 class='header6'></h6><ul class='list'><li class='item'></li></ul></div>";
  
  //Resig Template Function (modified to support ')
  function tmpl(str) {
              var strFunc =
              "var p=[];" +
                          "with(obj){p.push('" +
  
              str.replace(/[\r\t\n]/g, " ")
                 .replace(/'(?=[^#]*#>)/g, "\t")
                 .split("'").join("\\'")
                 .split("\t").join("'")
                 .replace(/<#=(.+?)#>/g, "',$1,'")
                 .split("<#").join("');")
                 .split("#>").join("p.push('")
                 + "');}return p.join('');";
  
              return new Function("obj", strFunc);
          }
  
  window.resig = tmpl("<div><h1 class='header'><#= header #></h1><h2 class='header2'><#= header2 #></h2><h3 class='header3'><#= header3 #></h3><h4 class='header4'><#= header4 #></h4><h5 class='header5'><#= header5 #></h5><h6 class='header6'><#= header6 #></h6><ul class='list'><# for (var i = 0, l = list.length; i < l; i++) { #><li class='item'><#= list[i] #></li><# } #></ul></div>");

window.qatrixTemplate = Qatrix.$template("<div><h1 class='header'>{{header}}</h1><h2 class='header2'>{{header2}}</h2><h3 class='header3'>{{header3}}</h3><h4 class='header4'>{{header4}}</h4><h5 class='header5'>{{header5}}</h5><h6 class='header6'>{{header6}}</h6></div>");

window.AAA = templateA.compile("<div><h1 class='header'><@-header@></h1><h2 class='header2'><@-header2@></h2><h3 class='header3'><@-header3@></h3><h4 class='header4'><@-header4@></h4><h5 class='header5'><@-header5@></h5><h6 class='header6'><@-header6@></h6></div>");
</script>

Test runner

Ready to run.

Testing in
TestOps/sec
Mustache
Mustache.to_html(mustacheTemplate, sharedVariables);
ready
jQuery.template
jQueryTemplate($, {
 data: sharedVariables
}).join("");
ready
Kendo UI
kendouiTemplate(sharedVariables);
ready
Handlebars
handlebarsTemplate(sharedVariables);
ready
Underscore
underscoreTemplate(sharedVariables);
ready
ART
AAA(sharedVariables)
ready
Qatrix
qatrixTemplate(sharedVariables);
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.