我对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><script></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>