不久前看过一篇不错的文章,作者用了15行代码就实现了一个简单的模板语法,我觉得很有趣,建议在阅读本文之前先看一下这个,本文不会讲解一些细节实现,这里是传送门:只有20行的Javascript模板引擎 这个模板语法实现的核心点是利用正则表达式来匹配到模板语法里面的变量和JS语句,遇到变量就将匹配到的字符串 push 到一个数组中,遇到 JS 语句就执行,最后再把数组中的字符串 join 起来,用 Function 来解析执行这串字符串,最终将执行后的结果放到指定 DOM 节点的innerHTML 里面。 但是这个模板语法还是有很多不足,比如不支持取余运算,不支持自定义模板语法,也不支持if、for、switch 之外的JS语句,缺少 HTML 实体编码。 恰好我这阵子也在看 underscore 源码,于是就参考了一下 underscore 中 template 方法的实现。 这个是我参考 template 后实现的模板,一共只有60行代码。
代码语言:javascript复制(function () {
var root = this;
// 将字符串中的HTML实体字符转义,可以有效减少xss风险
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) {
// 可以在外部修改template.templateSettings来自定义语法
// 一定要保证evaluate在最后,不然会匹配到<%=%>和<%-%>
var templateSettings = Object.assign({}, {
interpolate: /<%=([sS] ?)%>/g,
escape: /<%-([sS] ?)%>/g,
evaluate: /<%([sS] ?)%>/g,
}, template.templateSettings);
settings = Object.assign({}, settings);
// /<%=([sS] ?)%>|<%-([sS] ?)%>|<%([sS] ?)%>|$/g
// 其中$是为了匹配字符串的最后一个字符
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;'
// 使用with可以修改作用域
if (!settings.variable) source = "with(obj||{}) {n" source "n}"
var render = new Function(settings.variable || "obj", source);
return render
}
// 将templateY导出到全局
root.templateY = template
}.call(this))
转义
我们知道,在字符串中有一些特殊字符是需要转义的,比如"'", '"',不然就会和预期展示不一致,甚至是报错,所以我们一般会用反斜杠来表示转义,常见的转义字符有n, t, r等等。 但是这里的 convertEscapes 里面我们为什么要多加一个反斜杠呢? 这是因为在执行 new Function 里面的语句时,也需要对字符进行一次转义,可以看一下下面这行代码:
代码语言:javascript复制var log = new Function("var a = '1n23';console.log(a)");
log() // Uncaught SyntaxError: Invalid or unexpected token
这是因为 Function 函数在执行的时候,里面的内容被解析成了这样。
代码语言:javascript复制var a = '1
23';console.log(a)
在JS里面是不允许字符串换行出现的,只能使用转义字符n。
正则表达式
underscore 中摒弃了用正则表达式匹配 for/if/switch/{/} 等语句的做法,而是使用了不同的模板语法(<%=%>和<%%>)来区分当前是变量还是 JS 语句,这样虽然需要用户自己区分语法,但是给开发者减少了很多不必要的麻烦,因为如果用正则来匹配,那么后面就无法使用类似{##}的语法了。 这里正则表达式的重点是 ?, ?是惰性匹配,表示以最少的次数匹配到[sS],所以我们/<%=([sS] ?)%>/g是不会匹配到类似<%=name<%=age%>%>这种语法的,只会匹配到<%=name%>语法。
replace
这里我们用到了replace第二个参数是函数的情况。
代码语言:javascript复制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的性能会更高。 于是我这里进行了一下测试,在新版chrome中,下面这段代码中,两者耗时差不多,但是在v8中 =耗时要短于push。
代码语言:javascript复制var arr = [], str = "";
var i = 0, j = 0
console.time();
for(;i<100000;i ) {
arr.push(i);
}
arr.join("");
console.timeEnd()
console.time();
for(;j<100000;j ) {
str = j
}
console.timeEnd()
setting.variable
underscore这里使用with来改变了作用域,但是with会导致性能比较差,关于with的弊端可以参考一下这篇文章: Javascript中的with关键字 你还可以在variable设置里指定一个变量名,这样能显著提升模板的渲染速度。不过语法也和之前有一些不同,模板里面必须要用你指定的变量名来访问,而不能直接用answer这种形式,由于这种形式下没有使用with实现,所以性能会高很多。
代码语言:javascript复制_.template("Using 'with': <%= data.answer %>", {variable: 'data'})({answer: 'no'});
参考链接:
- js正则进阶
- JavaScript函数replace揭秘
- JavaScript正则表达式分组模式:捕获性分组与非捕获性分组及前瞻后顾
- underscore 系列之字符实体与 _.escape
- Javascript中的with关键字
- 高性能JavaScript模板引擎原理解析