很早就想研究underscore源码了,虽然underscore.js这个库有些过期了,可是我仍是想学习一下库的架构,函数式编程以及经常使用方法的编写这些方面的内容,又刚好没什么其它要研究的了,因此就告终研究underscore源码这一心愿吧。html
underscore.js源码研究(1)
underscore.js源码研究(2)
underscore.js源码研究(3)
underscore.js源码研究(4)
underscore.js源码研究(5)
underscore.js源码研究(6)
underscore.js源码研究(7)
underscore.js源码研究(8)react
参考资料:underscore.js官方注释,undersercore 源码分析,undersercore 源码分析 segmentfaultgit
以前就接触过模板引擎,好比说template.js、handlebars.js、jade.js、nunjucks.js等等。后来学react的时候也接触过jsx,当时我就感到很难以想象,居然可以把js中的变量甚至语句插入到html里面去,真的十分神奇。今天看underscore.js的源码的时候也发现里面居然有模板引擎,因而我就来研究研究模板引擎。正则表达式
说到模板引擎,一个最基本的特性就是能在html代码中插入js的变量,下面咱们来实现这种效果。编程
咱们须要实现这种效果:segmentfault
//定义一个模板 const tpl = 'hello {{name}}'; //定义值 const data = {name: 'haha'}; //渲染,最后content是name被替换过的html代码 const output = render(tpl, data);
其实仔细理了一下效果的流程以后,感受实现这个效果挺简单的,就是用一个正则替换,把{{ name }}
里面的值替换为data里面的数据就好了。api
实现代码以下:架构
//定义替换的正则表达式 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个要点:
因此好好整理一下,咱们首先须要语句的正则表达式,而后经过这个正则表达式按照上述规则进行替换,代码以下:
//为了方便,咱们把规则封装在一个对象里面 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为内容,定义一个函数,参数是obj const 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为内容,定义一个函数,参数是obj const renderFunc = new Function('obj', concating); return renderFunc(data); } //输出,结果为Students: haha yaya console.log(render(tpl, data));
能够看到,整个过程其实是在拼接和替换字符串,而后利用Function接受字符串的情形生成函数,没有其余的任何内容。
在下一篇博文中咱们会对这个小的模板引擎进行优化。