doTjs源码研究笔记

首先是入口方法

/*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 = { "&": "&#38;", "<": "&#60;", ">": "&#62;", '"': '&#34;', "'": '&#39;', "/": '&#47;' },
            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为函数体的函数

 至此完结了,我们只需要传入一个对象,就或获得结果了

posted @ 2014-06-10 03:11  迷途小哈  阅读(1447)  评论(1编辑  收藏  举报