模板引擎概述
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
underscore模板引擎实现
使用方式
var compiled = _.template("hello: <%= name %>");
compiled({name: 'moe'});
// => "hello: moe"
var template = _.template("<b><%- value %></b>");
template({value: '<script>'});
// => "<b><script></b>"
var compiled = _.template("<% print('Hello ' + epithet); %>");
compiled({epithet: "stooge"});
// => "Hello stooge"
源码解析
根据以上实现代码,_.template()方法会返回一个解析好的模板引擎函数compiled,再通过执行此函数,可获取到最终拼接后的数据。
开始源码读解:
_.templateSettings = {
evaluate: /<%([\s\S]+?)%>/g,
interpolate: /<%=([\s\S]+?)%>/g,
escape: /<%-([\s\S]+?)%>/g
};
var noMatch = /(.)^/;
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;
var escapeChar = function(match) {
return '\\' + escapes[match];
};
_.template = function(text, settings, oldSettings) {
if (!settings && oldSettings) settings = oldSettings;
/* 配置用户参数,如果settings里面有设置则用setting设置,否则用_.templateSettings设置
* _.templateSettings里配置的是三种正则模式
* _.defaults() 返回一个对象,前面对象若为undifined则会被后面对象所填充
*/
settings = _.defaults({}, settings, _.templateSettings);
/* 将三种正则模式合并成一种,利用[].join('|')的方法合并
* noMatch = /(.)^/,理论上此处不会用到
* .source是获取正则中的字符串
* matcher = /<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
* 最后加的$是为了匹配整个字符串的结尾,保留所有的字符串内容
*/
var matcher = RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
var index = 0;
var source = "__p+='";
/* 此处用到了replace(正则,function(){})的方法,function中依次是全匹配字符、子表达式1...x、匹配字符的偏移量
* 此处起到了一个循环的作用
*/
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
/* 根据示例match === '<%= name %>' interpolate === ' name ' 注意不好忽略空格
* source === "__p+='hello: "
* escapeChar 举例当text中遇到换行(\n)的时候,将其转换成'\n'字符串返回
*/
source += text.slice(index, offset).replace(escapeRegExp, escapeChar);
/* 同时将index从0变为偏移量加匹配项的长度
* 示例中offset === 7 => index = 7 + 11 = 18;
* 为后续继续替换做准备
*/
index = offset + match.length;
if (escape) {
/* 如果是逃逸字符,假设逃逸字符为<%- value %>
* source === "__p+='hello: '+\n((__t=(value))==null?'':_.escape(__t))+\n'"
*/
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
} else if (interpolate) {
/* 如果是插入值,根据示例<%= name %>
* source === "__p+='<b>'+\n((__t=(value))==null?'':__t)+\n'"
*/
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
} else if (evaluate) {
/* 如果是表达式,根据示例<% print('Hello ' + epithet); %>
* source === "__p+='';\n print('Hello ' + epithet); \n'__p+='"
*/
source += "';\n" + evaluate + "\n__p+='";
}
// 返回match,不改变原字符串
return match;
});
/* source === "__p+='hello: '+\n((__t=(value))==null?'':_.escape(__t))+\n'';\n"
* source === "__p+='<b>'+\n((__t=(value))==null?'':__t)+\n'</b>';\n"
* source === "__p+='';\n print('Hello ' + epithet); \n'__p+='';\n"
*/
source += "';\n";
// 如果settings.variable不存在,则将source包裹在with语句中,定义上下文
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
/* 并且在source之前定义__t,__p,__j和print = function(){__p+=__j.call(arguments,'');};
* 最后return __p
* 其中print是将参数无缝拼接成一个字符串,并加给__p
*/
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + 'return __p;\n';
var render;
try {
// new Function (上下文对象, 参数1...x,函数主体);
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
/* template是被返回函数,其中返回的是render中返回的函数
* 所以data是用户传过来的值
* data === {name: 'moe'} || {value: '<script>'} || {epithet: "stooge"}
*/
var template = function(data) {
// render.call,此处的this === window,并且传了两个参数,data和_
return render.call(this, data, _);
};
// 这一部分是给用户一个直观的渲染函数的输出,用户可通过template.source调用进行查看
var argument = settings.variable || 'obj';
template.source = 'function(' + argument + '){\n' + source + '}';
return template;
};
源码测试
根据示例1,打印出来的source和render分别如下:
// source
var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};
with(obj||{}){
__p+='hello: '+
((__t=( name ))==null?'':__t)+
'';
}
return __p;
// render
ƒ anonymous(obj,_
) {
var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};
with(obj||{}){
__p+='hello: '+
((__t=( name ))==null?'':__t)+
'';
}
return __p;
}
可知,render是一个匿名函数,将其格式化一下分析(尽量保留了原换行):
ƒ anonymous(obj, _) {
var __t, __p = '',
__j = Array.prototype.join,
// 这里只是定义了一个print方法,示例中并没有调用
print = function () { __p += __j.call(arguments, ''); };
// 发现\n都变成了回车
with (obj || {}) {
// 此处name === obj.name
// 所以__p === 'hello: moe'
__p += 'hello: ' +
((__t = (name)) == null ? '' : __t) +
'';
}
return __p;
}
把另外两种示例的render也打印出来分析:
ƒ anonymous(obj,_
) {
var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};
with(obj||{}){
__p+='<b>'+
((__t=( value ))==null?'':_.escape(__t))+
'</b>';
}
return __p;
}
ƒ anonymous(obj,_
) {
var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};
with(obj||{}){
__p+='';
// 调用了print,print将参数整合为一个字符串付赋值给__p,最后输出
// 所以__p === 'Hello stooge'
print('Hello ' + epithet);
__p+='';
}
return __p;
}
总结及技巧提炼
- text.replace(RegExp,function(){}) 可以依次获取其中匹配到的字符串加以利用,同时返回值是替换值,回调函数参数为:(match,[…sonMatch],offset)
- /|$/可以在使用的过程中保留最后一个匹配项到字符串结尾过程中的内容
- var render = new Function() 可以用来将字符串渲染为函数,其中参数为:(context,[…params],functionString)
- 正则对象.source可以获取其字符串
- 整体来说,source的拼接由于无法直观看到,所以容易晕。设计的时候肯定也需要反复推敲才好。
知识技能获取,感谢[网易云课堂 - 微专业 - 前端高级开发工程师]运营团队。