不久前看过一篇不错的文章,做者用了15行代码就实现了一个简单的模板引擎,我以为颇有趣,建议在读这篇文章以前先看一下这个,这里是传送门:只有20行的Javascript模板引擎html
这个模板引擎实现的核心点是利用正则表达式来匹配到模板语法里面的变量和JS语句,再将这些匹配到的字段push到一个数组中,最后链接起来,用Function来解析字符串,最后将执行后的结果放到指定DOM节点的innerHTML里面。git
可是这个模板引擎仍是有不少不足,好比不支持取余运算,不支持自定义模板语法,也不支持if、for、switch以外的JS语句,缺乏HTML实体编码。github
刚好我这阵子也在看underscore源码,因而就参考了一下underscore中template方法的实现。正则表达式
这个是我参考template后实现的模板,一共只有60行代码。segmentfault
(function () {
var root = this;
var html2Entity = (function () {
var escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'`': '`'
};
var escaper = function (match) {
return escapeMap[match];
};
return function (string) {
var source = "(" + Object.keys(escapeMap).join("|") + ")";
var regexp = RegExp(source), regexpAll = RegExp(source, "g");
return regexp.test(string) ? string.replace(regexpAll, escaper) : string;
}
}())
var escapes = {
'"': '"',
"'": "'",
"\\": "\\",
'\n': 'n',
'\r': 'r',
'\u2028': 'u2028',
'\u2029': 'u2029'
}
var escaper = /\\|'|"|\r|\n|\u2028|\u2029/g; var convertEscapes = function (match) { return "\\" + escapes[match]; } var template = function (tpl, settings) { var templateSettings = Object.assign({}, { interpolate: /<%=([\s\S]+?)%>/g, escape: /<%-([\s\S]+?)%>/g, evaluate: /<%([\s\S]+?)%>/g, }, template.templateSettings); settings = Object.assign({}, settings); var matcher = RegExp(Object.keys(templateSettings).map(function (key) { return templateSettings[key].source }).join("|") + "|$", "g") var source = "", index = 0; tpl.replace(matcher, function (match, interpolate, escape, evaluate, offset) { source += "__p += '" + tpl.slice(index, offset).replace(escaper, convertEscapes) + "'\n"; index = offset + match.length; if (evaluate) { source += evaluate + "\n" } else if (interpolate) { source += "__p += (" + interpolate + ") == null ? '' : " + interpolate + ";\n" } else if (escape) { source += "__p += (" + escape + ") == null ? '' : " + html2Entity(escape) + ";\n" } return match; }) source = "var __p = '';" + source + 'return __p;' if (!settings.variable) source = "with(obj||{}) {\n" + source + "\n}" var render = new Function(settings.variable || "obj", source); return render } root.templateY = template }.call(this)) 复制代码
咱们知道,在字符串中有一些特殊字符是须要转义的,好比"'", '"',否则就会和预期展现不一致,甚至是报错,因此咱们通常会用反斜杠来表示转义,常见的转义字符有\n, \t, \r等等。数组
可是这里的convertEscapes里面咱们为何要多加一个反斜杠呢?bash
这是由于在执行new Function里面的语句时,也须要对字符进行一次转义,能够看一下下面这行代码:函数
var log = new Function("var a = '1\n23';console.log(a)");
log() // Uncaught SyntaxError: Invalid or unexpected token
复制代码
这是由于Function函数在执行的时候,里面的内容被解析成了这样。性能
var a = '1 23';console.log(a)
复制代码
在JS里面是不容许字符串换行出现的,只能使用转义字符\n。ui
underscore中摒弃了用正则表达式匹配for/if/switch/{/}等语句的作法,而是使用了不一样的模板语法(<%=%>和<%%>)来区分当前是变量仍是JS语句,这样虽然须要用户本身区分语法,可是给开发者减小了不少没必要要的麻烦,由于若是用正则来匹配,那么后面就没法使用相似{# #}和{{}}的语法了。 这里正则表达式的重点是+?,+?是惰性匹配,表示以最少的次数匹配到[\s\S],因此咱们/<%=([\s\S]+?)%>/g是不会匹配到相似<%=name<%=age%>%>这种语法的,只会匹配到<%=name%>语法。
这里咱们用到了replace第二个参数是函数的状况。
var pattern = /([a-z]+)\s([a-z]+)/;
var str = "hello world";
str.replace(pattern, function(match, p1, p2, offset) {
// p1 is "hello"
// p2 is "world"
return match;
})
复制代码
在JS正则表达式中,使用()包起来的叫着捕获性分组,而使用(?:)的叫着非捕获性分组,在replace的第二个参数是函数时,每次匹配都会执行一次这个函数,这个函数第一个参数是pattern匹配到的字符串,在这个里面是"hello world"。
p1是第一个分组([a-z]+)匹配到的字符串,p2是第二个分组([a-z]+)匹配到的字符串,若是有更多的分组,那还会有更多参数p3, p4, p5等等,offset是最后一个参数,指的是在第几个索引处匹配到了,这里的offset是0,由于是从一开始就恰好匹配到了hello world。
underscore中使用+=字符串拼接的方式代替了数组push的方式,这样是由于+=相比push的性能会更高。
underscore这里使用with来改变了做用域,可是with会致使性能比较差,关于with的弊端能够参考一下这篇文章: Javascript中的with关键字
你还能够在variable设置里指定一个变量名,这样能显著提高模板的渲染速度。不过语法也和以前有一些不一样,模板里面必需要用你指定的变量名来访问,而不能直接用answer这种形式,这种形式下没有使用with实现,因此性能会高不少。
_.template("Using 'with': <%= data.answer %>", {variable: 'data'})({answer: 'no'});
复制代码
参考连接: