首先是入口方法
/*tmpl:模板文本 c:用户自定义配置 def:定义编译时执行的数据*/
doT.template = function(tmpl, c, def) {
}
然后进入第一句代码
c = c || doT.templateSettings;
doT.templateSettings包含的代码:
templateSettings: {
evaluate: /\{\{([\s\S]+?(\}?)+)\}\}/g,
interpolate: /\{\{=([\s\S]+?)\}\}/g,
encode: /\{\{!([\s\S]+?)\}\}/g,
use: /\{\{#([\s\S]+?)\}\}/g,
useParams: /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,
define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,
defineParams:/^\s*([\w$]+):([\s\S]+)/,//xxx(\w):xxx(.)
conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,
iterate: /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g,
varname: 'it',
strip: true,
append: true,
selfcontained: false
},
先不急着看正则是什么意思,理清思路先,继续往下看代码
var cse = c.append ? startend.append : startend.split,
这里定义了一个叫cse的变量,如果c.append为true,则它的值是startend.append,否则它的值是startend.split,看看startend是什么
var startend = {
append: { start: "'+(", end: ")+'", endencode: "||'').toString().encodeHTML()+'" },
split: { start: "';out+=(", end: ");out+='", endencode: "||'').toString().encodeHTML();out+='"}
},
OK,先不管它的作用,接着往下看
needhtmlencode, sid = 0, indv,
str = (c.use || c.define) ? resolveDefs(c, tmpl, def || {}) : tmpl;
在定义了cse这个变量后,相继又定义了needhtmlencode,sid,indv,str,其中str又牵涉到了resolveDefs这个函数:
function resolveDefs(c, block, def) {
return ((typeof block === 'string') ? block : block.toString())
.replace(c.define || skip, function(m, code, assign, value) {//code:def.def.def.xxx(\w) assign:':'|'=' value:xxx(.)
if (code.indexOf('def.') === 0) {
code = code.substring(4);//获取def后面的部分
}
if (!(code in def)) {
if (assign === ':') {
if (c.defineParams) value.replace(c.defineParams, function(m, param, v) {//如果def后面部分没有在传入的属性里面,则检测value部分
def[code] = {arg: param, text: v};//def.xx: xx:xx
});
if (!(code in def)) def[code]= value;//def.xx:xx
} else {
new Function("def", "def['"+code+"']=" + value)(def);//否则将value赋值给def
}
}
return '';
})
.replace(c.use || skip, function(m, code) {
if (c.useParams) code = code.replace(c.useParams, function(m, s, d, param) {
if (def[d] && def[d].arg && param) {
var rw = (d+":"+param).replace(/'|\\/g, '_');
def.__exp = def.__exp || {};
def.__exp[rw] = def[d].text.replace(new RegExp("(^|[^\\w$])" + def[d].arg + "([^\\w$])", "g"), "$1" + param + "$2");
return s + "def.__exp['"+rw+"']";
}
});
var v = new Function("def", "return " + code)(def);
return v ? resolveDefs(c, v, def) : v;
});
}
这个才一百多行代码的doT文件还真是耐嚼啊,分析一下这个函数的作用
1.首先将block转化为string类型
2.调用字符串替换方法,正则为c.define || skip,分析一下c.define这个正则
c.define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g
最外层匹配内容为{{##一些字符串#}},里面的内容依次是,\s*去掉一些乱打的空格,第一个捕获项([\w\.$]+),匹配abc123_.abc123_.这种形式的内容,然后\s*去掉一些空格,第二个捕获项(\:|=),匹配':'或者'=',第三个捕获项([\s\S]+?)什么都匹配
得出结论,c.define匹配的内容是{{##(数字字母下划线和点组成的随意组合)(:|=)(乱起八糟的一些东西)#}},三个括号分别代表不同的匹配项,
再看看匿名函数的形参,m对应的是被匹配的整个内容,code,assign,value分别代表三个不同的捕获项
还有个skip:/$^/;结束后马上开始?只有空字符才这样吧……意思是什么都不匹配,什么都没匹配意思是跳过了
3.if语句,如果code是以def.开头,则将def.后面的内容截取下来,赋值给code
4.if语句,如果code在def中没有定义,则继续执行
如果assign部分是':'的话继续执行
如果c.defineParams有定义,则将value部分进行字符串替换,正则是c.defineParams
c.defineParams:/^\s*([\w$]+):([\s\S]+)/
^\s*去掉开始的空格,([\w$]+)第一个捕获项,匹配多个数字字母下划线,然后碰到:,([\s\S]+)第二个捕获项,什么都匹配
匹配类(数字字母下划线):(乱起八糟的一些东西),匿名函数的形参m代表整个匹配项,param代表第一个捕获项,v代表第二个捕获项
匿名函数内部定义code,内容为{arg: param, text: v};
如果经过上面的代码code依然在def中没有定义,则将value作为值,在def中定义code
否则(如果assign是'=')
调用,new Function("def", "def['"+code+"']=" + value)(def);直接将value赋值给def[code],为什么用构造函数?因为这样value就能被求出来啦,就跟eval一样
总结一下,从2到4,做的事情是定义def.code的code部分,规则总结如下:
(1)def.code:a:b;def[code]={arg:a, text:b}
(2)def.code:a; def[code]=a;
(3)def.code=a;(这里a是表达式) def[code]=eval(a);
如果def中本就有code这个属性,那么上述三个过程都不会发生
5.经过上面的步骤,已经解析了{{## #}}了,并且都已经被替换为空,def对象中也会有相应属性
继续进行字符串替换,正则是c.use || skip,skip不用说了,直接看c.use
c.use:/\{\{#([\s\S]+?)\}\}/g
外壳:{{# }},([\s\S]+?),捕获项,而且啥都行,对应匿名函数参数,m是整个匹配项,code代表第一个捕获项,
if语句,如果c.useParams存在的话,对code进行字符串替换,并且正则就是c.useParams
c.useParams:/(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,
长啊……
(^|[^\w$])第一个捕获项,开始字符或者非数字字母下划线结尾符,
def,
(?:\.|\[[\'\"])非捕获项,匹配.或者['或者[",
第二个捕获项([\w$\.]+),匹配abc123_.abc123_.abc123_.,
(?:[\'\"]\])?,匹配']或者"],非贪婪匹配
\s*\:\s*,去掉空格,匹配:,
([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\}),第三个捕获项,匹配abc123_.abc123_.或者"xxxxxx"或者'xxxxxx',或者{xxxxxxx}
整个正则代表: (一些可能的特殊字符)def.(数字字母下划线和点的组合)[或者用['']|["包起来"]]:(数字字母下划线和点的组合|"乱七八糟"|'乱七八糟'|{乱七八糟})
m,s,d,param分别对应整个匹配项和三个捕获项
if语句,如果def[d]和def[d].arg和param都存在的话,拼接为d:param,并将其中的'和\都替换为_,赋值给rw变量
定义def.__exp[rw]为将def[d].text中的 特殊字符+def[d].arg+特殊字符 替换为 特殊字符+param+特殊字符 的形式
返回s + "def.__exp['"+rw+"']"
小结:def.d:param中,将d.text中符合arg的部分替换为param,并将d:param(\和'替换为_)作为唯一标识rw,赋值给def.__exp,返回s + "def.__exp['"+rw+"']"并将def.d:param替换,如果def[d]不存在的话就会被替换为undefined,
然后把已经解析完{{# }}形式的code(实际上是s + "def.__exp['"+rw+"']" 或者code)运行一下,并把def作为参数传入,结果用变量V存储
这里,我们可以知道
{{## #}}相当于定义模板,而{{# }}相当于解析模板
如果v为真,则递归调用resolveDefs,直到没有上面两种标签为止,最终,我们把去掉了上面两种标签的字符串存储到str变量中
这个函数可以说理解完了,作用是将{{## #}}中模板定义部分提取出来,并放到def对象中,然后解析{{# }},根据def中的定义,将其解析成普通文本
回到template方法中
str = ("var out='" + (c.strip ? str.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g,' ')
.replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,''): str)
一个逻辑,如果c.trip为假,则直接将该字符串拼接,否则进行一下字符串替换,两个正则
(1) (^|\r|\n)\t* +| +\t*(\r|\n|$)/g -> ' '
也就是说,所有在空格前后出现了一个回车换行制表开始或结束符号的,都被替换为一个空格
(2) /\r|\n|\t|\/\*[\s\S]*?\*\//g -> ''
这里将注释、回车、换行、制表符全部替换为空
这样基本把字符串多余的空格去掉了,但是没考虑两个字符之间很多空格的情况?……是假定没人这么做么?
继续往下看
.replace(/'|\\/g, '\\$&')
在将去掉空格的字符串与"var out="拼接后,再次执行了该字符串替换,作用是转义,将单引号反斜杠都加一个反斜杠,$&表示匹配的字符
.replace(c.interpolate || skip, function(m, code) {
return cse.start + unescape(code) + cse.end;
})
c.interpolate:/\{\{=([\s\S]+?)\}\}/g
首先最外层壳{{= }},然后是一个捕获项([\s\S]+?)啥都匹配的,形参分别为m代表整体, code代表壳内部的东西
cse就是那个startend中的一个属性,默认是cse.append
看看unescape
function unescape(code) {
return code.replace(/\\('|\\)/g, "$1").replace(/[\r\t\n]/g, ' ');
}
(1)./\\('|\\)/g:首先匹配\,然后匹配捕获项'或者\,$1代表第一个捕获项,也就是说把转义符去掉了
(2)./[\r\t\n]/g:换行符制表符回车符,全部变成一个空格
那么最终匿名函数内部返回的是'+(反转义并且去掉多于回车换行制表符的code)+',注意上面最开始去掉的是后面带有多个空格的回车换行制表
.replace(c.encode || skip, function(m, code) {
needhtmlencode = true;
return cse.start + unescape(code) + cse.endencode;
})
c.encode:/\{\{!([\s\S]+?)\}\}/g:外壳{{! }},捕获项啥都匹配,对应的匿名函数变量是code
匿名函数内部,首先将needhtmlencode赋值为true,然后返回的内容是'+(反转义并且去掉多于回车换行制表符的code||'').toString().encodeHTML()+'
.replace(c.conditional || skip, function(m, elsecase, code) {
return elsecase ?
(code ? "';}else if(" + unescape(code) + "){out+='" : "';}else{out+='") :
(code ? "';if(" + unescape(code) + "){out+='" : "';}out+='");
})
c.conditional:/\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,{{? }},第一个捕获项,捕获一个?或者没有,第二个捕获项为中间的去掉两头空格的部分(非贪婪匹配不会匹配后面出现的内容,这里后面是空格)
函数内部:如果第一个捕获项存在,code存在返回:"';}else if("反转义并且去掉多于回车换行制表符的code"){out+='",code不存在返回:"';}else{out+='"
如果第一个捕获想不存在,code存在返回:"';if("反转义并且去掉多于回车换行制表符的code"){out+='",code不存在返回:"';}out+='"
.replace(c.iterate || skip, function(m, iterate, vname, iname) {
if (!iterate) return "';} } out+='";
sid+=1; indv=iname || "i"+sid; iterate=unescape(iterate);
return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+"<l"+sid+"){"
+vname+"=arr"+sid+"["+indv+"+=1];out+='";
})
c.iterate:/\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g
{{~ }},去掉两边空格,
(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\}),非捕获项,首先匹配}}或者空格前的随机内容作为第一个捕获项,去掉空格,匹配:,去掉空格,匹配\w和$座位第二个捕获项,去掉空格,:,去掉空格,捕获\w和$作为第三个捕获项,去掉空格
匿名函数内部,如果第一个捕获项不存在(即出现{{~\s*}})的情况,则返回"';} } out+='",
sid自增,indv赋值为第三个匹配项或者'i'+sid,将第一个捕获项反转义,返回"';var arr"+sid+"="+第一个捕获项+";if(arr"+sid+"){var "+第二个捕获项+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+"<l"+sid+"){"+vname+"=arr"+sid+"["+indv+"+=1];out+='"
其实这里就是匹配{{~xx:xx[:xx]}}的情况,第三个xx没必要
.replace(c.evaluate || skip, function(m, code) {
return "';" + unescape(code) + "out+='";
}
c.evaluate:/\{\{([\s\S]+?(\}?)+)\}\}/g,{{}},捕获项为随意匹配,中间(\}?)+是为避免后面出现多余}的情况,返回"';" + unescape(code) + "out+='"
+ "';return out;")
将上面的内容都替换完后,将内容加上"';return out;"
总结一下,传入的内容中,首先解析的是{{## #}}和{{# }}这两种形式,作用如下
{{## #}}
(1)def.code:a:b;def[code]={arg:a, text:b},其中text可为表达式,而a为参数
(2)def.code:a; def[code]=a;
(3)def.code=a;(这里a是表达式) def[code]=eval(a);
{{# }}
(1){{#[^\w]def.code:param}}; 将def对象中的def[code]取出来,param作为其参数,运行def[code].text
(2){{#[^\w]def.code}};直接获取def中的code中的内容
然后定义str = "var out = '" + 去掉多余空格的字符串,并转义
{{=code}} => '+( unescape(code) )+'
{{!code}} => '+(unescape(code)||'').toString().encodeHTML()+' 同时 needhtmlencode = true
{{? }}
(1){{??code}} => ';}else if("unescape(code)"){out+='
(2){{??}} => ';}else{out+='
(3){{?code}} => ';if("unescape(code)"){out+='
(4){{?}} => ';}out+='
{{~}}
(1){{~}} => ';}} out+='
(2){{~a:b[:c]}} => "';var arr"+sid+"="+ a +";if(arr"+sid+"){var "+b+","+c+"=-1,l"+sid+"=arr"+sid+".length-1;while("+c+"<l"+sid+"){"+b+"=arr"+sid+"["+c+"+=1];out+='"
{{code}} => "';" + unescape(code) + "out+='"
+ "';return out;")
.replace(/\n/g, '\\n').replace(/\t/g, '\\t').replace(/\r/g, '\\r')
.replace(/(\s|;|\}|^|\{)out\+='';/g, '$1').replace(/\+''/g, '')
.replace(/(\s|;|\}|^|\{)out\+=''\+/g,'$1out+=');
(1)将\n,\r,\t转义
(2)去掉+''和out+='';的情况
(3)out+=''+ 替换为 out+=
if (needhtmlencode && c.selfcontained) {
str = "String.prototype.encodeHTML=(" + encodeHTMLSource.toString() + "());" + str;
}
如果存在{{!}}以及c.selfcontained为true,则在String的原型上添加encodeHTML
function encodeHTMLSource() {
var encodeHTMLRules = { "&": "&", "<": "<", ">": ">", '"': '"', "'": ''', "/": '/' },
matchHTML = /&(?!#?\w+;)|<|>|"|'|\//g;
return function() {
return this ? this.replace(matchHTML, function(m) {return encodeHTMLRules[m] || m;}) : this;
};
}
String.prototype.encodeHTML = encodeHTMLSource();
可以看到,这段代码是给html里的一些特殊符号编码,但是encodeHTML不是已经加上去了吗?为什么还要再弄一次……
其实,c.selfcontained配置为true之后,就可以每次定义新的encodeHTMLSource函数了
try {
return new Function(c.varname, str);
} catch (e) {
if (typeof console !== 'undefined') console.log("Could not create a template function: " + str);
throw e;
}
生成一个以c.varname为参数,str为函数体的函数
至此完结了,我们只需要传入一个对象,就或获得结果了