当前位置: 首页 > Web前端 > vue.js

Mustache的底层原理和简单实现

时间:2023-03-31 22:15:35 vue.js

用过Vue的都知道,我们可以在模板中使用{{xx}}来渲染数据中的属性。这种语法称为Mustache插值表达式。使用起来很简单,但是心里也有个疑问。它是怎么做到的?接下来就让我们一探究竟吧!1.使用正则表达式来实现比如有这样一个模板字符lettempStr2='我是{{develpoer}},我正在学习{{knowledge}}知识!';现在需要用数据替换字符串中的{{xxx}},那么可以使用正则表达式来实现lettempStr2='我是{{develpoer}},我正在学习{{知识}}知识!';letdata={develpoer:'web前端程序员',knowledge:'Mustache插值语法'};letresultStr=tempStr2.replace(/{{(\w+)}}/g,function(matched,$1){//{{develpoer}}develpoer//{{knowledge}}knowledgeconsole.log(matched,$1);returndata[$1];});//结果:我是一个web前端程序员,我正在学习Mustache插值语法知识!console.log('结果:',resultStr);使用正则表达式的缺点是只能实现简单的插值语法,无法实现循环、if判断等稍复杂的功能。2.Mustache底层思想:tokens思想lettempStr=`

    {{#students}}
  • {{name}}
    {{#hobbys}}
    {{.}}
    {{/hobbys}}
  • {{/students}}
`;遇到这样的模板字符串,按照我们之前的编程思路,大部分人想的肯定是如何获取{{#students}}和{{/students}}之间的内容,这是用正则表达式无法实现的,还有这串字符串想了半天还是没有结果。那么如果我们对这个字符串的内容进行分类呢?比如{{xxx}}归为一类,除{{xxx}}以外的普通字符串归为一类,存储在一个数组中。例如:这是代币的思想。当我们拿到这样一个数组后,我们就好办了,怎么拼接数据就轮不到你了。3.拆解模板字符串,分类思路(这里假设分隔符是一对{{}}):模板字符串中,使用变量或者使用遍历,if判断必须全部包裹在{{}}中普通的字符串都在{{的左边,所以可以通过查找{{的位置找到普通的字符串,然后截取{{位置前面的字符串已经被截取,当前模板字符串就变成了变成了{{xxx}}
  • ...,那么现在怎么得到xxx呢?新思路——使用字符串截取(别再考虑正则化了~)。{{前面的普通字符串之前已经被截取过,那么{{也可以被截取,{{被截取后,模板字符串变成xxx}}
  • ...xxx}}
  • ...这个字符串和原来的模板字符串类似,只是{{变成了}},那么我们可以和步骤2一样,找到}}的位置,然后截取截取xxx,把字符串变成}}
  • ...,那么我们就拦截}},然后回到第2步,一直重复,直到没有要拦截的字符串,那么代码就可以实现了:/***templatestringscandevice*是用来扫描分隔符{{}}左右两侧的普通字符串,获取{{}}中间的内容。(当然分隔符不一定是{{}})*/classScanner{constructor(templateStr){this.templateStr=templateStr;这个.pos=0;//找到字符串的指针位置this.tail=templateStr;//模板字符串的尾部}/***扫描模板字符串,跳过遇到的第一个匹配的定界符*@paramdelimiterReg*@returns{undefined}*/scan(delimiterReg){if(this.tail){letmatched=this.tail.match(delimiterReg);如果(!匹配){返回;}if(matched.index!=0){//分隔符的位置必须在字符串的开头,才能向后移动,否则会混淆return;}让delimiterLength=matched[0].length;this.pos+=delimiterLength;//指针位置需要加上分隔符的长度this.tail=this.tail.substr(delimiterLength);//控制台.log(this);}}/***扫描模板字符串,直到遇到第一个匹配的定界符,返回第一个定界符前的字符串(delimiterReg)*eg:*varstr='Iama{{develpoer}},andIamlearning{{知识}}知识!';*第一次运行:scanUtil(/{{/)=>'我是学生'*第二次运行:scanUtil(/{{/)=>'我正在学习'*@paramdelimiterRegseparatorregular*@returns{string}*/scanUtil(delimiterReg){//找到第一个分隔符在位置letindex=this.tail.search(delimiterReg);让匹配='';switch(index){case-1://没有找到,如果没有找到,说明后面没有使用mustache语法,那么把所有tail返回matched=this.tail;this.tail='';休息;case0://如果分隔符在开始位置,什么都不做break;default:/*如果找到第一个分隔符的位置,则截取第一个分隔符之前的字符串,将tail设置为找到的分隔符及其后的字符串,并更新指针位置*/matched=this.tail.substring(0,索引);this.tail=this.tail.substring(index);}this.pos+=matched.length;//console.log(this);返回匹配;}/***判断是否找到字符串结尾*@returns{boolean}*/eos(){returnthis.pos>=this.templateStr.length;}}出口{扫描仪};使用:import{Scanner}from'./Scanner';lettempStr=`
      {{#students}}
    • {{name}}
      {{#hobbys}}
      {{.}}
      {{/爱好}}</dl>
    • {{/students}}
    `;letstartDeli=/{{/;//开始分隔符letendDeli=/}}/;//结束分隔符letscanner=newScanner(tempStr);console.log(scanner.scanUtil(startDeli));//获取之前的普通字符串{{scanner.scan(startDeli);//跳过{{分隔符console.log(scanner.scanUtil(endDeli));//获取}}之前的字符串scanner.scan(endDeli);//跳过}}分隔符console.log('------------------------------------------');console.log(scanner.scanUtil(startDeli));//get{{front普通字符串scanner.scan(startDeli);//跳过{{分隔符console.log(scanner.scanUtil(endDeli));//获取}}之前的字符串scanner.scan(endDeli);//SkipPass}}delimiterresult:4.将字符串模板转换为tokens数组之前的Scanner已经可以解析字符串了,现在只需要组装模板字符串即可代码实现import{Scanner}from'../Scanner';/***模板字符串转token*@paramtemplateStr模板字符串*@paramdelimiters分隔符,其值为正则表达式,长度为2的表达式数组*@returns{*[]}*/exportfunctionparseTemplateToTokens(templateStr,delimiters=[/{{/,/}}/]){让[startDelimiter,endDelimiter]=delimiters;让标记=[];if(!templateStr){返回标记;}让扫描仪=新扫描仪(templateStr);while(!scanner.eos()){//获取开始分隔符前的字符串letbeforeStartDelimiterStr=scanner.scanUtil(startDelimiter);如果(beforeStartDelimiterStr.length>0){tokens.push(['文本',beforeStartDelimiterStr]);//console.log(beforeStartDelimiterStr);}//跳过开始定界符scanner.scan(startDelimiter);//获取开始分隔符和结束分隔符之间的字符串letafterEndDelimiterStr=scanner.scanUtil(endDelimiter);如果(afterEndDelimiterStr.length==0){继续;}if(afterEndDelimiterStr.charAt(0)=='#'){tokens.push(['#',afterEndDelimiterStr.substr(1)]);}否则如果(afterEndDelimiterStr.charAt(0)=='/'){tokens.push(['/',afterEndDelimiterStr.substr(1)]);}else{tokens.push(['name',afterEndDelimiterStr]);}//跳过结束分隔符scanner.scan(endDelimiter);}返回标记;}使用:从'./parseTemplateToTokens'导入{parseTemplateToTokens};lettempStr=`
      {{#students}}
    • {{name}}
      {{#hobbys}}
      {{.}}
      {{/hobbys}}
    • {{/students}}
    `;letdelimiters=[/{{/,/}}/];vartokens=parseTemplateToTokens(templateStr,delimiters);console.log(tokens);Result:5.将我们之前使用的模板字符重新组装在tokens中字符串中存在嵌套结构,前面组装的token是一维数组。显然不可能使用一维数组来渲染具有循环结构的模板字符串。即使可能,代码也很难理解。此时我们需要对一维数组进行再次组装,这次我们将其组装成一个嵌套结构,前面封装的一维数组也是符合条件的。代码:/***将平铺的令牌数组转换为嵌套令牌数组*@paramtokens一维令牌数组*@returns{*[]}*/exportfunctionnestsToken(tokens){varresultTokens=[];//结果集varstack=[];//堆栈数组varcollector=resultTokens;//结果收集器tokens.forEach(token=>{lettokenFirst=token[0];switch(tokenFirst){case'#'://当遇到#时,将当前token压入栈数组stack.push(token);collector.push(token);token[2]=[];//并将结果收集器设置为刚刚进入的堆栈中令牌的子集collector=token[2];break;case'/'://遇到/时,从栈数组中取出最新的stack.pop();//并收集结果收集器设置为栈数组中栈顶token的子集,或者最终的结构集合collector=stack.length>0?stack[stack.length-1][2]:resultTokens;break;default://如果不是#,/直接将当前token添加到结果集中collector.push(token);}});returnresultTokens;}调用后的结果:经过这一步,没有什么特别困难的,有了这样的结构,合并数据就很容易了。6.渲染模板下面的代码是我的简单实现:代码:import{lookup}from'./lookup';/***根据tokens将模板字符串渲染成html*@paramtokens*@paramdatasdata*@returns{string}*/functionrenderTemplate(tokens,datas){varresultStr='';tokens.forEach(tokenItem=>{vartype=tokenItem[0];vartokenValue=tokenItem[1];switch(type){case'text'://普通字符串可以直接拼接resultStr+=tokenValue;break;case'name'://获取对象属性//lookup用于以字符串的形式动态访问对象的深层属性方法,如:lookup({a:{b:{c:100}}},'a.b.c'),lookup({a:{b:{c:100}}},'a.b');resultStr+=lookup(datas,tokenValue);break;case'#':letvalueReverse=false;if(tokenValue.charAt(0)=='!'){//如果第一个字符是!,说明是在用if判断做反向操作tokenValue=tokenValue.substr(1);valueReverse=真;}letval=datas[tokenValue];resultStr+=parseArray(tokenItem,valueReverse?!val:val,datas);break;}});返回结果Str;}/***解析字符串模板中的循环*@paramtokentoken*@paramdatasData当前模板中循环需要的数据*@paramparentData上一层的数据*@returns{string}*/functionparseArray(token,datas,parentData){//console.log('parseArraydatas',datas);if(!Array.isArray(datas)){//如果data的值不是数组,则作为if判断处理letflag=!!datas;//如果值为真,渲染模板,否则返回空返回标志?renderTemplate(token[2],parentData):'';}varresStr='';datas.forEach(dataItem=>{//console.log('dataItem',dataItem);letnextData;if(({}).toString.call(dataItem)!='[object,Object]'){nextData={...dataItem,//添加一个“.”属性主要是为了在模板中使用{{.}}语法时使用'.':dataItem}}else{nextData={//添加一个"."属性,主要针对模板中使用{{.}}语法可以使用'.':dataItem};}resStr+=renderTemplate(token[2],nextData);});returnresStr;}export{renderTemplate,parseArray};使用:从'./parseTemplateToTokens'导入{parseTemplateToTokens};从'./nestsT导入{nestsToken}okens';从'./renderTemplate'导入{renderTemplate};lettempStr=`
      {{#students}}
    • {{name}}
      {{#hobbys}}
      {{.}}
      {{/hobbys}}
    • {{/students}}
    `;letdatas={students:[{name:'Html',hobbys:['超文本标记语言','网页结构'],age:1990,ageThen25:true,show2:true},{name:'Javascript',hobbys:['弱类型语言','动态脚本language','让页面动起来'],age:1995,ageThen25:0,show2:true},{name:'Css',hobbys:['层叠样式表','装饰性网页','排版'],age:1994,ageThen25:1,show2:true},]};letdelimiters=[/{{/,/}}/];vartokens=parseTemplateToTokens(templateStr,delimiters);console.log(tokens);varnestedTokens=nestsToken(令牌);控制台日志(nestedTokens);varhtml=renderTemplate(nestedTokens,datas);控制台日志(html);作用:7.{{}}中使用运算符的现有问题(如加减法,三元运算)不知函数如何实现?暂不支持流通8.EpilogueMustache的代币点子真不错!!!以后我们遇到类似的需求,也可以用它的思想来实现,而不是死死抱着正则表达式和字符串替换不放。