概述
很早就想研究underscore源碼了,雖然underscore.js這個庫有些過時了,但是我還是想學習一下庫的架構,函數式編程以及常用方法的編寫這些方面的內容,又恰好沒什么其它要研究的了,所以就了結研究underscore源碼這一心愿吧。
underscore.js源碼研究(1)
underscore.js源碼研究(2)
underscore.js源碼研究(3)
underscore.js源碼研究(4)
underscore.js源碼研究(5)
underscore.js源碼研究(6)
underscore.js源碼研究(7)
underscore.js源碼研究(8)
參考資料:underscore.js官方注釋,undersercore 源碼分析,undersercore 源碼分析 segmentfault
模板引擎
之前就接觸過模板引擎,比如說template.js、handlebars.js、jade.js、nunjucks.js等等。后來學react的時候也接觸過jsx,當時我就感到很不可思議,竟然能夠把js中的變量甚至語句插入到html里面去,真的十分神奇。今天看underscore.js的源碼的時候也發現里面竟然有模板引擎,于是我就來研究研究模板引擎。
實現變量替換
說到模板引擎,一個最基本的特性就是能在html代碼中插入js的變量,下面我們來實現這種效果。
我們需要實現這種效果:
//定義一個模板
const tpl = 'hello {{name}}';//定義值
const data = {name: 'haha'};//渲染,最后content是name被替換過的html代碼
const output = render(tpl, data);
其實仔細理了一下效果的流程之后,感覺實現這個效果挺簡單的,就是用一個正則替換,把{{ name }}
里面的值替換為data里面的數據就行了。
實現代碼如下:
//定義替換的正則表達式
const rule = /{{([\s\S]+?)}}/g;//render函數
function render(tpl, data) {return tpl.replace(rule, (matcher, p1) => {return data[p1];})
}
注意,由于rule里面只有一個括號,所以replace第二個參數的函數里面只有p1沒有p2。
變量替換改進
為了便于閱讀,我們需要模板可以寫成下面的形式。(name兩邊有空格)
//定義一個模板
const tpl = 'hello {{ name }}';
所以我們加一個去空格的函數,整個代碼如下:
//定義替換的正則表達式
const rule = /{{([\s\S]+?)}}/g;//render函數
function render(tpl, data) {return tpl.replace(rule, (matcher, p1) => {return data[p1.trim()];})
}
注意:trim函數只兼容IE9,如果要兼容IE9以下的話就需要pollyfill了。
支持語句
幾乎所有的模板引擎都支持寫入語句,比如像下面的寫法:
const tpl = 'Students:' +//注意這里只有一個大括號!!!'{ for(i = 0; i < data.students.length; i++) }' +'{{ data.students[i].name }}';
const data = {students: [{id: 1,name: ' haha '},{id: 2,name: ' yaya '}]
};
const content = render(tpl, data);
我們希望上述代碼輸出:
Students: haha yaya
看起來非常復雜,可是我們用偽代碼分解一下執行過程就感覺有點簡單了:
//首先輸出Students:
//然后執行下面的代碼
for(i = 0; i < data.students.length; i++) {
//這里輸出students[i].name
}
//完畢
實際寫起來是這樣的:
//定義要輸出的內容
content = '';
content += 'Students:';
for(i = 0; i < data.students.length; i++) {content += 'data.students[i].name';
}
//輸出整個content
return content
可以看到,上面有這2個要點:
- 變量的內容需要添加到content里面,但是語句的內容不需要添加到content里面。
- 不是模板的內容要記錄位置,然后再通過這個位置添加到content里面。
所以好好整理一下,我們首先需要語句的正則表達式,然后通過這個正則表達式按照上述規則進行替換,代碼如下:
//為了方便,我們把規則封裝在一個對象里面
const rules = {//插值,對應變量interpolate: /{{([\s\S]+?)}}/,//邏輯,對應語句evaluate: /{([\s\S]+?)}/
};
//2個正則合在一起,先替換變量,再替換語句
const matcher = new RegExp([rules.interpolate.source,rules.evaluate.source
].join('|'), 'g');//render函數
function render(tpl, data) {let concating = 'let content = "";\n';let index = 0;//仍然是replace里面的第二個參數是函數的形式tpl.replace(matcher, (match, interpolate, evaluate, offset) => {//添加非模板的內容if (tpl.slice(index, offset)) {concating += 'content += "' + tpl.slice(index, offset) + '";\n';}//記錄偏移量index = offset + match.length;//變量需要添加到content里面if (interpolate) {concating += 'content +=' + interpolate + ';\n';//語句不需要添加到content里面,而且不要分號} else if (evaluate) {concating += evaluate + '\n';}})concating += 'return content;';//以concating為內容,定義一個函數,參數是objconst renderFunc = new Function('obj', concating);return renderFunc(data);
}
它生成的renderFunc函數的代碼如下圖所示:
(function(obj
/*``*/) {
let content = "";
content += "Students:";for(i = 0; i < data.students.length; i++)
content += data.students[i].name ;
return content;
})
可以看到有一個缺點,就是for循環沒有大括號,這就導致它只執行下面的那條語句。如果要加大括號的話,就需要額外的規則,我們這里不討論。
所以把上面所有的代碼加起來就是這樣的:
//為了方便,我們把規則封裝在一個對象里面
const rules = {//插值,對應變量interpolate: /{{([\s\S]+?)}}/,//邏輯,對應語句evaluate: /{([\s\S]+?)}/
};//2個正則合在一起,先替換變量,再替換語句
const matcher = new RegExp([rules.interpolate.source,rules.evaluate.source
].join('|'), 'g');//定義模板和數據
const tpl = 'Students:' +//注意這里只有一個大括號!!!'{ for(i = 0; i < data.students.length; i++) }' +'{{ data.students[i].name }}';
const data = {students: [{id: 1,name: ' haha '},{id: 2,name: ' yaya '}]
};//render函數
function render(tpl, data) {let concating = 'let content = "";\n';let index = 0;//仍然是replace里面的第二個參數是函數的形式tpl.replace(matcher, (match, interpolate, evaluate, offset) => {//添加非模板的內容if (tpl.slice(index, offset)) {concating += 'content += "' + tpl.slice(index, offset) + '";\n';}//記錄偏移量index = offset + match.length;//變量需要添加到content里面if (interpolate) {concating += 'content +=' + interpolate + ';\n';//語句不需要添加到content里面,而且不要分號} else if (evaluate) {concating += evaluate + '\n';}})concating += 'return content;';//以concating為內容,定義一個函數,參數是objconst renderFunc = new Function('obj', concating);return renderFunc(data);
}//輸出,結果為Students: haha yaya
console.log(render(tpl, data));
可以看到,整個過程實際上是在拼接和替換字符串,然后利用Function接受字符串的情形生成函數,沒有其他的任何內容。
在下一篇博文中我們會對這個小的模板引擎進行優化。