关于模板
关于模板,写页面的人们其实一直在用,asp.net , jsp , php, nodejs等等都有他的存在,当然那是服务端的模板。
前端模板,作为前端人员肯定是多少有接触的,
Handlebars.js,JsRender,Dust.js,Mustache.js,Underscore templates,Angularjs,Vuejs,reactjs到处都离不开模板的影子。
Micro-Templating解析在线测试
Micro-Templating模板
本文主要是分析一下jQuery的创始人的Micro-Templating,麻雀虽小缺五张俱全。
到这里可别笑,说用什么jQuery的,jQuery的思想绝对是异常的优秀,霸榜十多年, React也不过是最近两年才赶超。
先贴出作者的源码:
// Simple JavaScript Templating // John Resig - https://johnresig.com/ - MIT Licensed (function(){ var cache = {}; this.tmpl = function tmpl(str, data){ // Figure out if we're getting a template, or if we need to // load the template - and be sure to cache the result. var fn = !/\W/.test(str) ? cache[str] = cache[str] || tmpl(document.getElementById(str).innerHTML) : // Generate a reusable function that will serve as a template // generator (and which will be cached). new Function("obj", "var p=[],print=function(){p.push.apply(p,arguments);};" + // Introduce the data as local variables using with(){} "with(obj){p.push('" + // Convert the template into pure JavaScript str .replace(/[\r\t\n]/g, " ") .split("<%").join("\t") .replace(/((^|%>)[^\t]*)'/g, "$1\r") .replace(/\t=(.*?)%>/g, "',$1,'") .split("\t").join("');") .split("%>").join("p.push('") .split("\r").join("\\'") + "');}return p.join('');"); // Provide some basic currying to the user return data ? fn( data ) : fn; }; })(); 复制代码
基本原理:
- 使用属性检查来进行缓存
- 采用正则替换标签(赋值标签,js语句标签)
- 使用with设置代码在对象中的作用域,主要是提升了编程体验,(当然也可以用apply,call,bind等修改函数作用域,然后通过this.prop来编写,但是体验上差一些)
- 动态构建执行函数
- 通过判断决定返回结果类型
关于 1,3,5没有太多需要讲的,关于5,如果执行时不传入data参数,返回的执行函数,可以延迟使用,到处使用。
调试的日志输出。
重点在于2和4,在这之前,先看看print,这个print申请在函数顶部,就表示在js语句的时候是可以调用呢,怎么调用呢,看看示例,至于作用么,当然是debug啊
<script type="text/html" id="item_tmpl"> <% for ( var i = 0; i < items.length; i++ ) { %> <% if( i%2 == 1) {%> <li><%=items[i].id%>:<%=items[i].name%></li> <% } %> <% } %> <% print('数组长度' + items.length ); %> <div style='background:<%=color%>'><%=id%></div> </script> 复制代码
很简单: <% print('数组长度' + items.length ); %>
原理也很简单,数组p里面添加一条数据
正则替换
为了方便debug和备注,我调整一下原理结构
(function () { var cache = {}; this.tmpl = function tmpl(str, data) { // Figure out if we're getting a template, or if we need to // load the template - and be sure to cache the result. var fn = !/\W/.test(str) ? cache[str] = cache[str] || tmpl(document.getElementById(str).innerHTML) : // Generate a reusable function that will serve as a template // generator (and which will be cached). new Function("obj", "var p=[],print=function(){p.push.apply(p,arguments);};" + // Introduce the data as local variables using with(){} "with(obj){p.push('" + // Convert the template into pure JavaScript getStr(str) + "');}return p.join('');"); // Provide some basic currying to the user return data ? fn(data) : fn; }; function getStr(str){ // 删除回车,制表,换行 str = str .replace(/[\r\t\n]/g, " "); // 替换 <% 为 \t制表符,两种情况(赋值和js代码) // 赋值: 例如 <div id="<%=id%>"> ==> <div id="\t=id%>"> // js代码:例如 <% for ( var i = 0; i < items.length; i++ ) { %> ==> \t for ( var i = 0; i < items.length; i++ ) { %> str = str.split("<%").join("\t"); // 替换'为\r ,最后一步会重新替换回来 // 节点属性操作赋值使用单引号,如果不替换 ,''>' 是会报错的 // <div style='background:<%=color%>'><%=id%></div> ==> p.push(' <div style='background:',color,''>',id,'</div> '); str = str.replace(/((^|%>)[^\t]*)'/g, "$1\r"); // 赋值解析:赋值后部分,拆分为三项,结合with,id就会成为实际的值,然后一直被push <div id="\t=id%>"> ==> <div id=" ,id, "> // 这里会消费掉 <%=xxx%>, // 那么剩下的 %>必然是js语句结尾的, \t必然是js语句的开头 str = str.replace(/\t=(.*?)%>/g, "',$1,'"); //js语句开始符号替换: 经过上一步后,还剩余的\t,是js语句的,这里就用 ');来结束 ,js语句会单开p.push, str = str.split("\t").join("');"); // js语句结尾符号替换: %> 替换为 p.push, 这里把js语句内生成的字符串或者变量再push一次 str = str.split("%>").join("p.push('"); // 替换回车为\' , 恢复str.replace(/((^|%>)[^\t]*)'/g, "$1\r") 去掉的' str = str.split("\r").join("\\'"); return str; } })(); 复制代码
上面很有意思的是,先完全替换了\r\t,然后再用\r\t作为占位符。 \t作为<%的占位符,\r作为特定条件下'的占位符。
逐步分析
我们接下来按照正则替换一步异步来分析
模板源码
<% for ( var i = 0; i < items.length; i++ ) { %> <% if( i%2 == 0) {%> <li><%=items[i].id%>:<%=items[i].name%></li> <% } %> <% } %> <% print('数组长度' + items.length ); %> <div style='background:<%=color%>'><%=id%></div> 复制代码
第零步:等于源码,只是把\n显示出来
\n <% for ( var i = 0; i < items.length; i++ ) { %> \n <% if( i%2 == 0) {%>\n <li><%=items[i].id%>:<%=items[i].name%></li>\n <% } %> \n <% } %>\n <% print('数组长度' + items.length ); %>\n <div style='background:<%=color%>'><%=id%></div>\n 复制代码
第一步: replace(/[\r\t\n]/g, " ")
去掉回车,换行,制表
<% for ( var i = 0; i < items.length; i++ ) { %> <% if( i%2 == 0) {%> <li><%=items[i].id%>:<%=items[i].name%></li> <% } %> <% } %> <% print('数组长度' + items.length ); %> <div style='background:<%=color%>'><%=id%></div> 复制代码
第二步: split("<%").join("\t")
<%替换为\t
\t for ( var i = 0; i < items.length; i++ ) { %> \t if( i%2 == 0) {%> <li>\t=items[i].id%>:\t=items[i].name%></li> \t } %> \t } %> \t print('数组长度' + items.length ); %> <div style='background:\t=color%>'>\t=id%></div> 复制代码
第三步: replace(/((^|%>)[^\t]*)'/g, "$1\r")
替换需要保留的'为\r, 主要是节点属性操作
\t for ( var i = 0; i < items.length; i++ ) { %> \t if( i%2 == 0) {%> <li>\t=items[i].id%>:\t=items[i].name%></li> \t } %> \t } %> \t print('数组长度' + items.length ); %> <div style=\rbackground:\t=color%>\r>\t=id%></div> 复制代码
第四步: replace(/\t=(.*?)%>/g, "',$1,'")
赋值部分替换,',$1,',实际是把赋值部分独立出来,那么push到这里的时候,就会进行运算
\t for ( var i = 0; i < items.length; i++ ) { %> \t if( i%2 == 0) {%> <li>',items[i].id,':',items[i].name,'</li> \t } %> \t } %> \t print('数组长度' + items.length ); %> <div style=\rbackground:',color,'\r>',id,'</div> 复制代码
第五步: split("\t").join("');")
剩下的\t,代表了js语句开始部分, js语句\t替换为'); ,正是push的结束部分,正好完成push语句
'); for ( var i = 0; i < items.length; i++ ) { %> '); if( i%2 == 0) {%> <li>',items[i].id,':',items[i].name,'</li> ');} %> '); } %> '); print('数组长度' + items.length ); %> <div style=\rbackground:',color,'\r>',id,'</div> 复制代码
第六步: split("%>").join("p.push('");
剩下的%>体表了js语句的结束,替换为p.push('",开启新的环节
'); for ( var i = 0; i < items.length; i++ ) { p.push(' '); if( i%2 == 0) {p.push(' <li>',items[i].id,':',items[i].name,'</li> '); } p.push(' '); } p.push(' '); print('数组长度' + items.length ); p.push(' <div style=\rbackground:',color,'\r>',id,'</div> 复制代码
第七部: split("\r").join("\'")
替换\r为' , 恢复str.replace(/((^|%>)[^\t]*)'/g, "$1\r") 去掉的'
'); for ( var i = 0; i < items.length; i++ ) { p.push(' '); if( i%2 == 0) {p.push(' <li>',items[i].id,':',items[i].name,'</li> '); } p.push(' '); } p.push(' '); print('数组长度' + items.length ); p.push(' <div style=\'background:',color,'\'>',id,'</div> 复制代码
加上头尾
var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push(' '); for ( var i = 0; i < items.length; i++ ) { p.push(' '); if( i%2 == 0) {p.push(' <li>',items[i].id,':',items[i].name,'</li> '); } p.push(' '); } p.push(' '); print('数组长度' + items.length ); p.push(' <div style=\'background:',color,'\'>',id,'</div> ');}return p.join(''); 复制代码
最后格式化一下
var p = [], print = function () { p.push.apply(p, arguments); }; with (obj) { p.push(' '); for (var i = 0; i < items.length; i++) { p.push(' '); if (i % 2 == 0) { p.push(' < li > ', items[i].id, ': ', items[i].name, '</li > '); } p.push(' '); } p.push(' '); print('数组长度' + items.length); p.push(' < div style =\'background:', color, '\'>', id, '</div> '); } return p.join(''); 复制代码
split + join VS replace
源码中你会发现,时而replace,时而split + join,大家都很清楚的可以看出 split + join达到的效果是和replace完全一致的。说到这里,大家肯定都很明白了,效率
我简单做了个实验,源码如下,自行替换str的值,然后贴到控制台执行,我测试的内容是打开百度, 查看源码,把所有源码赋值过来,然后执行。
var str = ` blabla...................................... ` + Math.random(); console.log('str length:' + str.length) console.log('a count:' + str.match(/a/g).length) console.time('split-join-a') str.split('a').join('_a_') console.timeEnd('split-join-a') console.time('replace-a') str.replace(/a/g,'_a_') console.timeEnd('replace-a') console.log('window count:' + str.match(/window/g).length) console.time('split-join-window') str.split('window').join('_window_') console.timeEnd('split-join-window') console.time('replace-window') str.replace(/window/g,'_window_') console.timeEnd('replace-window') 复制代码
执行结果
a count:4364 split-join-a: 4.521240234375ms replace-a: 13.24609375ms window count:29 split-join-window: 0.330078125ms replace-window: 0.297119140625ms 复制代码
11万个字符,
当匹配项是4000多得时候,执行时间相差比较大 ,
当匹配项是29的时候,知晓效率相差并不大,很多时候,replace比split+join还快
注意注意,这里都是不带正则查找,建议就是匹配项多得时候,用split +join喽
能用否
这个模板如此简单,能不能担任重任。这是基于字符串模板,还有基于dom的模板,还有混合型的。 字符串模板的缺点抛开安全和性能,就是渲染后和页面分离了,要想再操作,就需要自己再去定制了。 假如是仅仅是列表展现,是相当好的。