我的js模板引擎reMarker第一版(循环遍历算法,非正则)
by simon4545
at 2010-12-14 17:03:00
original http://www.cnblogs.com/simon4545/archive/2010/12/14/1905855.html
首先介绍一下reMarker出现的原因:
1.公司里新做了一个公网项目,使用的是java+json+javscript+html的前后台结合的机制,后端生成json代码,前端所有的页面渲染基本使用js的模板引擎,引擎生成了html后
和Json数据集成,渲染成最后的页面。
2.因为Java开发人员都比较熟悉freemark模板的语法,为了减少学习成本,我们使用js的模板引擎用的也是freemark的语法。而我所在的公司有一个风格,几乎不用公网的代码,
所有的代码基本上从前到后都是员工自已开发的。
3.而且后来经过我的实际测试,互联网上的一些模板引擎,jTemplate/jst/sweetTemplate/ext template也的确在效率上并不能高过我所开发的这个模板引擎。
下面列出的这个算法,也并不是什么高明的东西。其创意来自于一个ooCSS的面向对象CSS处理机制。
4.我们可以看到目前公网上的js模板引擎用的都是正则来处理特征,进行选择匹配,最后动态生成function,返回替换后的字符串。而我们知道javascript/java/c#等语言用的都
是NFA(非确定型有穷自动机)正则引擎,它的优点是longest-leftmost匹配,回溯,组捕获等,但同时他相对于DFA(确定型有穷自动机)正则引擎的缺点也就出现了,速度相对慢
一点,虽然是native code,但是我并不是很信任他的效率。后来做过一过不成熟的测试,发现它的确会比一般的字符串遍历寻找要慢(事实证明,我的这个测试代码并不科学,
下次会分析)。
所以我就在这里写下了这样一个模板引擎,支持freemark语法,可以拓展?html/?html/?default等扩展方法在util对象中。
就像上面所说的,他是有bug的,但我并没有打算修复,毕竟我只是为了测试效率才写的。当然这个bug并不复杂,如果你喜欢,可以去修正一下,估计十几分钟就Ok了,谁的代
码能没有bug呢?残念。。。
经过测试,实现我的demo page中相同的功能,主流的引擎没有发现比这个更快的(除了我马上要出的下一版,呵呵).谢谢
/*
这个版本是有bug的,如果freemark语法不是模板的最后一段,会出现bug
但是它是使用数组循环的方法,为了和以后的正则比较效率,会保持这个版本不删除!
*/
(function(){
if (window.reMarker || window.r) {
throw "reMarker is exist"
};
window.reMarker = window.r = m = function(tpl, data){
this.tpl = tpl;
this.data = data;
this.scope = null;
this.vars = [];
}
window.r.regRuler = {
ruler: function(str){
var listArr = r.util.removeEmpty(str.split(' '));
/*这里如果不用function而直接return回替换值的话,千次执行会快出5毫秒,和机器有关 switch(listArr[0]){
case "list": return this.rulerList(listArr);break;
case "if": return 'if(' + listArr.slice(1).join('') + '){';break;
case "break": return 'break;';break;
case '/#list': return '}})();';break;
case 'else': return '}else{';break;
case "/#if": return '}';break;
case 'elseif':return '}else if(' + listArr.slice(1).join('') + '){';break;
case 'switch':return 'switch(' + listArr.join('').replace('switch','') + '){';break;
case 'case': return ('case ' + listArr[1] + ':');break;
case 'default':return 'default:';break;
case '/#switch': return '}';
}
*/
var ruler = {
"list": this.rulerList,
"if": this.rulerIf,
"break": this.rulerBreak,
'/#list': this.rulerEndList,
'else': this.rulerElse,
"/#if": this.rulerEndIf,
'elseif': this.rulerElseIf,
'switch': this.rulerSwitch,
'case': this.rulerCase,
'default': this.rulerDefault,
'/#switch': this.rulerEndSwitch
};
return (ruler[listArr[0]]).call(this, listArr);
},
rulerEndSwitch:function(arr){
return '}';
},
rulerCase:function(arr){
return ('case ' + arr[1] + ':');
},
rulerDefault:function(){
return 'default:'
},
rulerSwitch:function(arr){
return 'switch(' + arr.join('').replace('switch','') + '){'
},
rulerElseIf:function(){
if(arr.length<2){return false;}
return '}else if(' + arr.slice(1).join('') + '){';
},
rulerBreak:function(){
return 'break;'
},
rulerElse:function(arr){
return '}else{';
},
rulerEndIf:function(arr){
return '}';
},
rulerIf: function(arr){
if(arr.length<2){return false;}
return 'if(' + arr.slice(1).join('') + '){'
},
rulerEndList:function(arr){
return '}})();';
},
rulerList:function(arr){
var listName, loopName, loopIndexName, loopHasNextName, result = [];
if(arr.length!=4){return;}
loopName=arr[3];
listName=arr[1];
loopIndexName = loopName + '_index';
loopHasNextName = loopName + '_has_next';
result.push('(function(){');
if (!/^\w+$/.test(listName)) {
result.push('var _list=' + listName + ';');
listName = '_list';
}
result.push(['var _i=0', '_count=' + listName + '.length', loopName, loopIndexName, loopHasNextName + ';'].join(','));
result.push('for(;_i<_count;_i++){');
result.push(loopName + '=' + listName + '[_i];');
result.push(loopIndexName + '=_i;');
result.push(loopHasNextName + '=_i!==_count-1;');
return result.join('');
}
}
window.r.util = {
trim: function(str){
return this.replace(/(^\s*)|(\s*$)/g, "");
},
lTrim: function(str){
return this.replace(/(^\s*)/g, "");
},
rTrim: function(str){
return this.replace(/(\s*$)/g, "");
},
removeEmpty: function(arr){
return arr.join(',').replace(',,', ',').split(',');
}
}
m.prototype.process = function(data, scope){
return this.splitVar(this.tpl);
};
m.prototype.splitVar = function(tpl){
var chunks = [], idx = 0, lastIndex = 0, le = tpl.length;
var self = this;
var printPrefix = "__buf__.push(";
var replaced = [];
tpl=tpl.replace("\r\n",'').replace("\t",'');
function pushStr(str){
str = str.replace(/'/g, "\\'");
if (str !== '') {
replaced.push(printPrefix + '\'' + str + '\');');
}
}
var peek = function(tok){
if (tok == "<") {
if (tpl[idx + 1] == "#" && tpl[idx + 2] == "-" && tpl[idx + 2] == "-") {
findEnd(">");
chunks.push(tpl.substring(lastIndex, idx));
pushStr(chunks[chunks.length-1].replace("#",'!'));
return true;
}
else
if (tpl[idx + 1] == "#") {
findEnd(">");
chunks.push(tpl.substring(lastIndex, idx));
replaced.push(r.regRuler.ruler(chunks[chunks.length-1].slice(2, -1)));
return true;
}
else
if (tpl[idx + 1] == "/" && tpl[idx + 2] == "#") {
findEnd(">");
chunks.push(tpl.substring(lastIndex, idx));
replaced.push(r.regRuler.ruler(chunks[chunks.length-1].slice(1, -1)))
return true;
}
}
else if(tok=="$") {
if (tpl[idx + 1] == "{") {
findEnd("}");
chunks.push(tpl.substring(lastIndex, idx));
replaced.push(printPrefix + chunks[chunks.length-1].slice(2, -1) + ');');
return true;
}
}else{
return false;
}
}
var findEnd = function(tok){
for (var i = idx+2 ; i < le; i++) {
if (tpl[i] == tok) {
//增加非语法句
if (tpl[lastIndex] != '<') {
chunks.push(tpl.substring(lastIndex, idx))
pushStr(chunks[chunks.length-1]);
}
lastIndex = idx;
idx = i + 1;
break;
}
}
}
while (idx < le) {
if (peek(tpl[idx])) {
//增加语法句
lastIndex = idx;
}
else {
idx++;
}
}
chunks.push(tpl.substring(lastIndex));
replaced.push(tpl.substring(lastIndex))
replaced = ["var __buf__=[],$index=null;$util.print=function(str){__buf__.push(str);};with($data){", replaced.join(''), "} return __buf__.join('');"].join('');
//need 4ms when the function is null
this.compiled = new Function("$data", "$util", replaced);
var util = {};
if (m.util) {
var _util = m.util;
for (var key in _util) {
util[key] = _util[key];
}
}
return this.compiled.call(this.scope || window, this.data, util);
};
m.prototype.log = function(){
if (typeof(console) !== "undefined") {
for (var i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
};
})();
但是它是使用数组循环的方法,为了和以后的正则比较效率,会保持这个版本不删除!
*/
(function(){
if (window.reMarker || window.r) {
throw "reMarker is exist"
};
window.reMarker = window.r = m = function(tpl, data){
this.tpl = tpl;
this.data = data;
this.scope = null;
this.vars = [];
}
window.r.regRuler = {
ruler: function(str){
var listArr = r.util.removeEmpty(str.split(' '));
/*这里如果不用function而直接return回替换值的话,千次执行会快出5毫秒,和机器有关 switch(listArr[0]){
case "list": return this.rulerList(listArr);break;
case "if": return 'if(' + listArr.slice(1).join('') + '){';break;
case "break": return 'break;';break;
case '/#list': return '}})();';break;
case 'else': return '}else{';break;
case "/#if": return '}';break;
case 'elseif':return '}else if(' + listArr.slice(1).join('') + '){';break;
case 'switch':return 'switch(' + listArr.join('').replace('switch','') + '){';break;
case 'case': return ('case ' + listArr[1] + ':');break;
case 'default':return 'default:';break;
case '/#switch': return '}';
}
*/
var ruler = {
"list": this.rulerList,
"if": this.rulerIf,
"break": this.rulerBreak,
'/#list': this.rulerEndList,
'else': this.rulerElse,
"/#if": this.rulerEndIf,
'elseif': this.rulerElseIf,
'switch': this.rulerSwitch,
'case': this.rulerCase,
'default': this.rulerDefault,
'/#switch': this.rulerEndSwitch
};
return (ruler[listArr[0]]).call(this, listArr);
},
rulerEndSwitch:function(arr){
return '}';
},
rulerCase:function(arr){
return ('case ' + arr[1] + ':');
},
rulerDefault:function(){
return 'default:'
},
rulerSwitch:function(arr){
return 'switch(' + arr.join('').replace('switch','') + '){'
},
rulerElseIf:function(){
if(arr.length<2){return false;}
return '}else if(' + arr.slice(1).join('') + '){';
},
rulerBreak:function(){
return 'break;'
},
rulerElse:function(arr){
return '}else{';
},
rulerEndIf:function(arr){
return '}';
},
rulerIf: function(arr){
if(arr.length<2){return false;}
return 'if(' + arr.slice(1).join('') + '){'
},
rulerEndList:function(arr){
return '}})();';
},
rulerList:function(arr){
var listName, loopName, loopIndexName, loopHasNextName, result = [];
if(arr.length!=4){return;}
loopName=arr[3];
listName=arr[1];
loopIndexName = loopName + '_index';
loopHasNextName = loopName + '_has_next';
result.push('(function(){');
if (!/^\w+$/.test(listName)) {
result.push('var _list=' + listName + ';');
listName = '_list';
}
result.push(['var _i=0', '_count=' + listName + '.length', loopName, loopIndexName, loopHasNextName + ';'].join(','));
result.push('for(;_i<_count;_i++){');
result.push(loopName + '=' + listName + '[_i];');
result.push(loopIndexName + '=_i;');
result.push(loopHasNextName + '=_i!==_count-1;');
return result.join('');
}
}
window.r.util = {
trim: function(str){
return this.replace(/(^\s*)|(\s*$)/g, "");
},
lTrim: function(str){
return this.replace(/(^\s*)/g, "");
},
rTrim: function(str){
return this.replace(/(\s*$)/g, "");
},
removeEmpty: function(arr){
return arr.join(',').replace(',,', ',').split(',');
}
}
m.prototype.process = function(data, scope){
return this.splitVar(this.tpl);
};
m.prototype.splitVar = function(tpl){
var chunks = [], idx = 0, lastIndex = 0, le = tpl.length;
var self = this;
var printPrefix = "__buf__.push(";
var replaced = [];
tpl=tpl.replace("\r\n",'').replace("\t",'');
function pushStr(str){
str = str.replace(/'/g, "\\'");
if (str !== '') {
replaced.push(printPrefix + '\'' + str + '\');');
}
}
var peek = function(tok){
if (tok == "<") {
if (tpl[idx + 1] == "#" && tpl[idx + 2] == "-" && tpl[idx + 2] == "-") {
findEnd(">");
chunks.push(tpl.substring(lastIndex, idx));
pushStr(chunks[chunks.length-1].replace("#",'!'));
return true;
}
else
if (tpl[idx + 1] == "#") {
findEnd(">");
chunks.push(tpl.substring(lastIndex, idx));
replaced.push(r.regRuler.ruler(chunks[chunks.length-1].slice(2, -1)));
return true;
}
else
if (tpl[idx + 1] == "/" && tpl[idx + 2] == "#") {
findEnd(">");
chunks.push(tpl.substring(lastIndex, idx));
replaced.push(r.regRuler.ruler(chunks[chunks.length-1].slice(1, -1)))
return true;
}
}
else if(tok=="$") {
if (tpl[idx + 1] == "{") {
findEnd("}");
chunks.push(tpl.substring(lastIndex, idx));
replaced.push(printPrefix + chunks[chunks.length-1].slice(2, -1) + ');');
return true;
}
}else{
return false;
}
}
var findEnd = function(tok){
for (var i = idx+2 ; i < le; i++) {
if (tpl[i] == tok) {
//增加非语法句
if (tpl[lastIndex] != '<') {
chunks.push(tpl.substring(lastIndex, idx))
pushStr(chunks[chunks.length-1]);
}
lastIndex = idx;
idx = i + 1;
break;
}
}
}
while (idx < le) {
if (peek(tpl[idx])) {
//增加语法句
lastIndex = idx;
}
else {
idx++;
}
}
chunks.push(tpl.substring(lastIndex));
replaced.push(tpl.substring(lastIndex))
replaced = ["var __buf__=[],$index=null;$util.print=function(str){__buf__.push(str);};with($data){", replaced.join(''), "} return __buf__.join('');"].join('');
//need 4ms when the function is null
this.compiled = new Function("$data", "$util", replaced);
var util = {};
if (m.util) {
var _util = m.util;
for (var key in _util) {
util[key] = _util[key];
}
}
return this.compiled.call(this.scope || window, this.data, util);
};
m.prototype.log = function(){
if (typeof(console) !== "undefined") {
for (var i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
};
})();
测试代码在这里,只要把上面的js另存为remarker2.js放在demo page同一目录下就Ok了。用firefox调试
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<title>template engine -reMarker </title>
<script type="text/javascript" src="reMarker2.js"></script>
</head>
<body>
<div id="tpl">
<pre id="tpl_pre"></pre>
</div>
<div id="text"></div>
</body>
<script type="text/javascript">
var html=['<p>name: ${name}</p>',
'<p>age: ${age}</p>',
'<p>sex: ${sex}</p>',
'<p>fav:</p><ul>',
'<#list fav as f>',
'<li>${f}</li>',
'</#list>',
'</ul>',
'<#--list language as being-->',
'<p>language:</p><ul>',
'<#list language as being>',
'<li>${being_index + 1} - ',
'<span style="color:<#if being.level < 1 >red<#else>black</#if>">',
'${being.name}: ${being.level}</span>',
'</li>',
'</#list>',
'</ul>',
'<#if like><span style="color: red">Yes, we like it!</span>',
'<#else><span style="color: green">No, we don\'t like it!</span></#if>'].join('');
var data ={
name: "mark one",
age: 28,
sex: "male",
fav: ["game", "swim", "programming"],
language: [{
name: "C#",
level: 2
}, {
name: "javascript",
level: 3
}, {
name: "java",
level: 2
}, {
name: "python",
level: 1
}],
like: true
};
var te;
console.profile();
var marker=new reMarker(html,data);
te=marker.process()
console.profileEnd();
document.write(te)
//marker.process();
</script>
</html>
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<title>template engine -reMarker </title>
<script type="text/javascript" src="reMarker2.js"></script>
</head>
<body>
<div id="tpl">
<pre id="tpl_pre"></pre>
</div>
<div id="text"></div>
</body>
<script type="text/javascript">
var html=['<p>name: ${name}</p>',
'<p>age: ${age}</p>',
'<p>sex: ${sex}</p>',
'<p>fav:</p><ul>',
'<#list fav as f>',
'<li>${f}</li>',
'</#list>',
'</ul>',
'<#--list language as being-->',
'<p>language:</p><ul>',
'<#list language as being>',
'<li>${being_index + 1} - ',
'<span style="color:<#if being.level < 1 >red<#else>black</#if>">',
'${being.name}: ${being.level}</span>',
'</li>',
'</#list>',
'</ul>',
'<#if like><span style="color: red">Yes, we like it!</span>',
'<#else><span style="color: green">No, we don\'t like it!</span></#if>'].join('');
var data ={
name: "mark one",
age: 28,
sex: "male",
fav: ["game", "swim", "programming"],
language: [{
name: "C#",
level: 2
}, {
name: "javascript",
level: 3
}, {
name: "java",
level: 2
}, {
name: "python",
level: 1
}],
like: true
};
var te;
console.profile();
var marker=new reMarker(html,data);
te=marker.process()
console.profileEnd();
document.write(te)
//marker.process();
</script>
</html>
作者: simon4545 发表于 2010-12-14 17:03 原文链接
最新新闻:
· 专访甲骨文高管:Java虚拟机将支持更多动态语言 公布移动版路线图(2010-12-14 22:57)
· 甲骨文宣布成立北京云计算解决方案中心(2010-12-14 22:49)
· 我们的宇宙不是唯一的(2010-12-14 22:22)
· 法国竞争局警告谷歌不要滥用市场主导地位(2010-12-14 22:21)
· 旅行者1号即将飞离太阳系(2010-12-14 22:20)
编辑推荐:WP7有约(二):课后作业