不久前看過一篇不錯的文章,做者用了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'});
複製代碼
參考連接: