对_.template函数的理解

我对underscore.js中的_.template函数进行了学习总结:

 

这个函数算得上是一个轻量级的模板引擎(包括注释80多行);

模板引擎简单来说就是在动态渲染页面的时候,可以简化字符串的拼接操作。可以帮助我们有效地组织页面的结构和底层逻辑,也是实现界面和数据分离的重要一个步骤;

 

一.如何使用:

该函数可以解析3种模板标签:

<%=  %> 标签中包含的通常是变量名、函数名、对象属性,执行时直接展现调用后的数据

 

var templateString = _.template("<div><%=name%></div>");
    templateString({
       name:"lzj",
    })// ==><div>lzj</div>

 

 

<%-   %> 标签在输出数据时,能将HTML标记转成常用字符串形成(专门去解析html元素其实是为了防止xss攻击)

 

var templateString = _.template("<div><%-name%></div>");
    templateString({
        name:"<script>",
    })//==><div>&lt;script&gt;</div>

 

 

<%    %> 标签中包含的通常是JavaScript代码,在页面渲染数据时被执行;这个应该是最实用的一个方法,这里看不出来,但当它和<%= %>结合起来的时候还是很不错的;

 

 

 

var templateString = _.template("<div><%var a=1;a=a+b;console.log(a)%></div>");
   templateString({
      b:2              
  })//==>3

 

简单举一个例子,说明一下优势:比如我想动态生成3个li,如果用常规的方法如下:

 

 

var people=[1,2,3];
(function(){
    var temp="";
      for(var i=0;i<people.length;i++){
       temp+="<li>"+people[i]+"</li>"
    }
})()

 

 

如果用模板引擎的话如下:

 

var obj={
   people:[1,2,3]
}
var templateString='<% for(var i=0;i<people.length;i++){%>\
                        <li><%=people[i]%></li>\
                    <%}%>'
templateString=_.template(templateString);
var dom=templateString(obj)//==><li>1</li><li>2</li><li>3</li>

 

 

如果变量再多一点,那么字符串拼接的" " 将会亮瞎你的双眼;当我们有了这种模板之后,动态生成html元素就方便很多了,也很有利于插件的编写和维护;不用再一个个document.createElement ,setAttribute了;

 

总结一下:(如果我们想动态生成一个html标签添加到页面上去)

我们首先定义一个字符串,就跟我们平时写html标签一样,只不过遇到变量的时候用<%%>括起来 至于是用<%=%>  <%-%> <%%>这三个中的哪一个就参考上面的例子;

 

var string='<p class=<%=className%> id=<%=idName%>>\
                <%=name%>\
            </p>';

 

 

 

然后利用_.template去生成我们的字符串模板:

 

var obj={
    className:"p-class",
    idName:"p-id",
    name:"liuzj"
 }
var templateString=_.template(string); //_.template调用后是生成了一个函数,我们需要再次调用这个函数,才能得到我们需要的字符串
    templateString=templateString(obj);

通过上面两步,我们最后生成了"<p class=p-class id=p-id>liuzj</p>" 这样一个字符串,然后我们就可以通过innterHTML来将我们的字符串真正转化为我们需要的dom元素,渲染到页面上面去;

 

 

ps:如果需要对元素绑定事件的话,我们可以在innerHTML成功之后,通过ID等选择器去进行绑定;

 

 

二.分析实现原理

看上去template模板引擎功能不是很复杂,但是实现起来并不容易,至少在此之前我尝试失败了;而且我不看源码可能也不会想到会这样去实现吧,还能学习到一些其他的JS知识;

我们这边就以上面的例子先通过打印,来看一下到底是怎么操作的;

var string="<p><%=name%></p>"

当我们定义了这样一个字符串的时候,需要对里面的name这个变量进行替换,首先我们就得先解析这个字符串,将变量和常量区分开;这一步需要通过正则表达式来完成:

/**********a.定义正则**********/

_.templateSettings = {
                                   evaluate : /<%([\s\S]+?)%>/g,

                                   interpolate : /<%=([\s\S]+?)%>/g,

                                   escape : /<%-([\s\S]+?)%>/g

                                  };

上面这个正则分别对应着三种情况:<%%>  <%=%>  <%-%>

然后将其整合:

    var matcher = RegExp([
      (settings.escape || noMatch).source,
      (settings.interpolate || noMatch).source,
      (settings.evaluate || noMatch).source
    ].join('|') + '|$', 'g');

打印一下这个matcher:

/<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g

 

/*********b.将字符串进行替换拼接,保存至source这个变量*********/

然后我们利用replace来对传进来的字符串进行解析;

源码为:

 

  var index = 0;
  var source = "__p+='";
  text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
      console.log("match:"+match);
      console.log("escape:"+escape);
      console.log("interpolate:"+interpolate);
      console.log("evaluate:"+evaluate);
      console.log("offset:"+offset);
      source += text.slice(index, offset).replace(escaper, escapeChar);
      console.log("source1:"+source);
      index = offset + match.length; 
      console.log(index) 
      if (escape) { 
          source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 
      } else if (interpolate) { 
          source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
      } else if (evaluate) { 
          source += "';\n" + evaluate + "\n__p+='"; 
      }
      console.log("source2:"+source);
     return match; 
});

 

这里额外说一下当replace的第二个参数当做函数时的用法:


当replace方法执行的时候每次都会调用该函数,返回值作为替换的新值

第一个参数为每次匹配的全文本($&)。

中间参数为子表达式匹配字符串,个数不限.( $i (i:1-99))

倒数第二个参数为匹配文本字符串的匹配下标位置。

最后一个参数表示字符串本身。

*这里稍微提一下,上面这个例子中'最后一个参数'是偏移量,这与我们描述的冲突?其实是因为它省略了其匹配的字符串本身这个参数,它没有写而已。

 

我添加了打印,一起从打印结果来分析一下源码:

 

/******第一次*********/
match:<%=name%>              //匹配到了我们需要替换的地方
escape:undefined             //这里没匹配到<%-%> 所以undefined
interpolate:name             //这里匹配到了<%=%> 所以能够得到我们的子表达式匹配的字符串
evaluate:undefined           //这里没有匹配到<%%> 所以undefined
offset:3                     //这个偏移是第一次匹配到<%=name%>时的偏移
source1:__p+='<p>            //这里的source是至关重要的,后面主要是利用这个变量进行解析赋值,先获取到了匹配'<%=name%>'之前的常量 '<p>'
index:12                     //获取字符串匹配结束位置之后的开始位置索引
source2:__p+='<p>'+          //将常量和变量再进行一次组合
((__t=(name))==null?'':__t)+ //对于三种不同的情况有三种不同的解析方法,这里是变量,只是简单的替换,对于html元素会进行一次<  >等符号转义
'                            //对于js的解析则是在后面再加一个__p,__p是什么,后面解析的时候会说
/******第二次*********/
match:                       //因为是/g全局匹配,所以会比匹配到的结果次数多一次,使'字符串指针'最终到整个字符串结尾;
escape:undefined             //第二次主要是处理匹配到字符串结束后的字符串常量
interpolate:undefined
evaluate:undefined
offset:16
source1:__p+='<p>'+
((__t=(name))==null?'':__t)+
'</p>
index:16
source2:__p+='<p>'+          //最终的source结果
((__t=(name))==null?'':__t)+
'</p>


最后再收一个尾

source += "';\n";

我们的终极source就成了这个样子:

 

__p+='<p>'+
((__t=(name))==null?'':__t)+
'</p>';

 

/*********c.将source再次封装成一个具体的js执行代码字符串*******/

接下来的工作就是如何去处理source了,继续来看源码;

 

// If a variable is not specified, place data values in local scope.如果没有指定变量,则将数据值放在本地范围内。(这里是默认true)
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';

 

我们这里就有必要了解一下 with的用法了;
with 语句用于设置代码在特定对象中的作用域。
例如:

var obj={
  a:"aaa"
}

console.log(a) //undefined 会报错,因为全局作用域中没有a这个变量,window中也没这个属性


这个时候with就派上用处了

var obj={
  a:"aaa"
}
with(obj){
 console.log(a) //aaa 这个时候当它找不到a的时候回去obj中去寻找
}

在这个地方我们就是将source里面的代码在执行时指定在特定对象中的作用域


接下来我们就将source完全打造成一段”字符串式执行代码“

source = "var __t,__p='',__j=Array.prototype.join," + "print=function(){__p+=__j.call(arguments,'');};\n" + source + 'return __p;\n';

我们再来看看经过改造的source:

var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};
with(obj||{}){
__p+='<p>'+
((__t=(name))==null?'':__t)+
'</p>';
}
return __p;

 

/**********d.利用new Function 将source'字符串代码'封装成一个render函数***********/

接着:

 

try {
        var render = new Function(settings.variable || 'obj','_',source);
     } catch (e) {
        e.source = source;
        throw e;
     }

 

这里我们又有了一个新的知识点: new Function:
它是用来生成一个匿名函数,参数是以字符串的形式传入 var function_name = new Function(arg1, arg2, ..., argN, function_body)
每个 arg 都是一个参数,最后一个参数是函数主体(要执行的代码)

var fn=new Function('a','console.log(a)')
fn("liuzj") //liuzj

它和eval其实是功能差不多的,也是会将其输入的字符串当做js代码来执行。  区别于:eval能访问上下文,new Function只能构建自己的一个私有作用域。
看下面这个题目就知道了:

 

var a,b,c;
(function(){
  eval('var b = 2');
  (1, eval)('var c = 3');// 逗号操作符,括号表达式,返回的是最后一个挂载在window上的eval
  (new Function('var a = 4'))();
  document.write('<br>a: ' + a);
  document.write('<br>b: ' + b);
  document.write('<br>c: ' + c);
})()
document.write('<br>a: ' + a);
document.write('<br>b: ' + b);
document.write('<br>c: ' + c);

结果是:
a: undefined
b: 2
c: 3
a: undefined
b: undefined
c: 3

所以这里源码用new Function而不用eval是为了防止污染作用域吧;
不过这两种方法都用得很少,不看源码我可能都不知道这两种方法;new Function在这里使用,eval的话应用场景应该更少一点:https://www.zhihu.com/question/20591877
 

 

OK,学习了新知识后,我们接着源码分析;
我们知道这里将source"封装"在了render函数中,我们打印一下看看

(function(obj,_
/**/) {
var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};
with(obj||{}){
__p+='<p>'+
((__t=(name))==null?'':__t)+
'</p>';
}
return __p;
})

 

/****e.导出一个template函数用来zh**执行我们的render函数*/

最后返回一个函数,我们最后调用这个函数的时候就相当于在调用render函数;

var template = function(data) {
  return render.call(this, data, _);
};
var argument = settings.variable || 'obj';
template.source = 'function(' + argument + '){\n' + source + '}';
return template;

 

附加:

1). 至于前面说过对于JS的处理方式和其他两个不一样,这里说一下:

我们以这个模板为例<%console.log(name) %>

解析出来的source是这个样子:

 

__p+='';
console.log(name) 
__p+='';


对于js的模板我们是直接去执行它,所以它并不拼接到__p里面,__p是我们最后输出的字符串。所以源码这样处理,在后面再加了一个'__p+=':

source += "';\n" + evaluate + "\n__p+='"; 

 

2).对于多行字符串的处理,我这边为了使得代码好看一点,形成'树形代码的样式' ,所以我采用的是"  " 配合\

'<div>\
   <%=name%>\
</div>\'

 

不过这样会带来一些不必要的麻烦,比如会带来额外的换行符(\t)和空格(\s);

 

<div>   liuzj   </div>

这样就对我们获取名字的时候会有一些麻烦的地方,需要手动清除首尾的\t,而且打开F12看的时候也不美观;

所以这里我们可换一种形式:

[
   '<div>',
      '<%=name%>',
  '</div>'
].join("")

或者

'<div>'+
    '<%=name%>'+
'</div>'

 

 

 

<div>liuzj</div>

 

 

 

 




版权声明:本文为liuzijiang1123原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。